From 5b73c606452398567ef32aed434eec2eb52d4c18 Mon Sep 17 00:00:00 2001 From: Teknosquad5219 Date: Tue, 17 Dec 2024 17:46:40 -0500 Subject: [PATCH 01/69] Componentalized a lot of things that will not fit in this commit message --- .../competition/InsightsAndSettingsCard.tsx | 120 +++++++++++++----- .../[seasonSlug]/[competitonSlug]/index.tsx | 100 +-------------- 2 files changed, 87 insertions(+), 133 deletions(-) diff --git a/components/competition/InsightsAndSettingsCard.tsx b/components/competition/InsightsAndSettingsCard.tsx index 8cd8f25b..03375467 100644 --- a/components/competition/InsightsAndSettingsCard.tsx +++ b/components/competition/InsightsAndSettingsCard.tsx @@ -1,89 +1,139 @@ -import { NotLinkedToTba } from "@/lib/client/ClientUtils"; +import { download, NotLinkedToTba } from "@/lib/client/ClientUtils"; import { defaultGameId } from "@/lib/client/GameId"; import { Round } from "@/lib/client/StatsMath"; import { games } from "@/lib/games"; -import { Competition, Pitreport, Report, Team } from "@/lib/Types"; +import { Competition, MatchType, Pitreport, Report, Team } from "@/lib/Types"; import Link from "next/link"; -import { ChangeEvent } from "react"; +import { ChangeEvent, useState } from "react"; import { BsGearFill, BsClipboard2Check } from "react-icons/bs"; import { FaSync, FaBinoculars, FaUserCheck, FaDatabase } from "react-icons/fa"; import { FaUserGroup } from "react-icons/fa6"; +import ClientApi from "@/lib/api/ClientApi"; + +const api = new ClientApi(); export default function InsightsAndSettingsCard(props: { - showSettings: boolean; - setShowSettings: (value: boolean) => void; isManager: boolean | undefined; comp: Competition | undefined; reloadCompetition: () => void; assignScouters: () => void; - exportAsCsv: () => void; - exportPending: boolean; showSubmittedMatches: boolean; toggleShowSubmittedMatches: () => void; assigningMatches: boolean; regeneratePitReports: () => void; - newCompName: string | undefined; - setNewCompName: (value: string) => void; - newCompTbaId: string | undefined; - setNewCompTbaId: (value: string) => void; - saveCompChanges: () => void; redAlliance: number[]; setRedAlliance: (value: number[]) => void; - blueAlliance: number[]; - setBlueAlliance: (value: number[]) => void; matchNumber: number | undefined; setMatchNumber: (value: number) => void; - createMatch: () => void; - teamToAdd: number; - setTeamToAdd: (value: number) => void; - addTeam: () => void; submittedReports: number | undefined; reports: Report[]; loadingScoutStats: boolean; pitreports: Pitreport[]; submittedPitreports: number | undefined; - togglePublicData: (e: ChangeEvent) => void; seasonSlug: string | undefined; team: Team | undefined; allianceIndices: number[]; }) { + const [showSettings, setShowSettings] = useState(false); const { - showSettings, - setShowSettings, isManager, comp, reloadCompetition, assignScouters, - exportAsCsv, - exportPending, showSubmittedMatches, toggleShowSubmittedMatches, assigningMatches, - newCompName, - setNewCompName, - newCompTbaId, - setNewCompTbaId, - saveCompChanges, redAlliance, setRedAlliance, - blueAlliance, - setBlueAlliance, matchNumber, setMatchNumber, - createMatch, - teamToAdd, - setTeamToAdd, - addTeam, submittedReports, reports, loadingScoutStats, pitreports, submittedPitreports, - togglePublicData, seasonSlug, team, allianceIndices, } = props; + const [newCompName, setNewCompName] = useState(comp?.name); + const [newCompTbaId, setNewCompTbaId] = useState(comp?.tbaId); + const [exportPending, setExportPending] = useState(false); + const [teamToAdd, setTeamToAdd] = useState(0); + const [blueAlliance, setBlueAlliance] = useState([]); + + const exportAsCsv = async () => { + setExportPending(true); + + const res = await api.exportCompAsCsv(comp?._id!).catch((e) => { + console.error(e); + return { csv: undefined }; + }); + + if (!res) { + console.error("failed to export"); + } + + if (res.csv) { + download(`${comp?.name ?? "Competition"}.csv`, res.csv, "text/csv"); + } else { + console.error("No CSV data returned from server"); + } + + setExportPending(false); + }; + + const createMatch = async () => { + try { + await api.createMatch( + comp?._id!, + Number(matchNumber), + 0, + MatchType.Qualifying, + blueAlliance as number[], + redAlliance as number[], + ); + } catch (e) { + console.error(e); + } + + location.reload(); + }; + + async function saveCompChanges() { + // Check if tbaId is valid + if (!comp?.tbaId || !comp?.name || !comp?._id) return; + + let tbaId = newCompTbaId; + const autoFillData = await api.competitionAutofill(tbaId ?? ""); + if ( + !autoFillData?.name && + !confirm(`Invalid TBA ID: ${tbaId}. Save changes anyway?`) + ) + return; + + await api.updateCompNameAndTbaId( + comp?._id, + newCompName ?? "Unnamed", + tbaId ?? NotLinkedToTba, + ); + location.reload(); + } + + function togglePublicData(e: ChangeEvent) { + if (!comp?._id) return; + api.setCompPublicData(comp?._id, e.target.checked); + } + + function addTeam() { + console.log("Adding pit report for team", teamToAdd); + if (!teamToAdd || teamToAdd < 1 || !comp?._id) return; + + api + .createPitReportForTeam(teamToAdd, comp?._id) + // We can't just pass location.reload, it will throw "illegal invocation." I don't know why. -Renato + .finally(() => location.reload()); + } return (
diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx index 95d80fdf..041cd2d1 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx @@ -49,9 +49,9 @@ export default function CompetitionIndex({ team?.owners.includes(session?.user?._id)) ?? false; - const [showSettings, setShowSettings] = useState(false); + const [matchNumber, setMatchNumber] = useState(undefined); - const [blueAlliance, setBlueAlliance] = useState([]); + const [redAlliance, setRedAlliance] = useState([]); const [matches, setMatches] = useState([]); @@ -105,11 +105,6 @@ export default function CompetitionIndex({ string | undefined >(); - const [teamToAdd, setTeamToAdd] = useState(0); - - const [newCompName, setNewCompName] = useState(comp?.name); - const [newCompTbaId, setNewCompTbaId] = useState(comp?.tbaId); - const regeneratePitReports = useCallback(async () => { console.log("Regenerating pit reports..."); const { pitReports: pitReportIds } = await api.regeneratePitReports( @@ -348,23 +343,6 @@ export default function CompetitionIndex({ } }; - const createMatch = async () => { - try { - await api.createMatch( - comp?._id!, - Number(matchNumber), - 0, - MatchType.Qualifying, - blueAlliance as number[], - redAlliance as number[], - ); - } catch (e) { - console.error(e); - } - - location.reload(); - }; - // useEffect(() => { // if ( // qualificationMatches.length > 0 && @@ -395,29 +373,6 @@ export default function CompetitionIndex({ loadMatches(matches !== undefined); } - const [exportPending, setExportPending] = useState(false); - - const exportAsCsv = async () => { - setExportPending(true); - - const res = await api.exportCompAsCsv(comp?._id!).catch((e) => { - console.error(e); - return { csv: undefined }; - }); - - if (!res) { - console.error("failed to export"); - } - - if (res.csv) { - download(`${comp?.name ?? "Competition"}.csv`, res.csv, "text/csv"); - } else { - console.error("No CSV data returned from server"); - } - - setExportPending(false); - }; - useEffect(() => { if (ranking || !comp?.tbaId || !team?.number) return; @@ -438,46 +393,11 @@ export default function CompetitionIndex({ useInterval(() => loadMatches(true), 5000); - function togglePublicData(e: ChangeEvent) { - if (!comp?._id) return; - api.setCompPublicData(comp?._id, e.target.checked); - } - function remindUserOnSlack(userId: string) { if (userId && team?._id && isManager && confirm("Remind scouter on Slack?")) api.remindSlack(team._id.toString(), userId); } - function addTeam() { - console.log("Adding pit report for team", teamToAdd); - if (!teamToAdd || teamToAdd < 1 || !comp?._id) return; - - api - .createPitReportForTeam(teamToAdd, comp?._id) - // We can't just pass location.reload, it will throw "illegal invocation." I don't know why. -Renato - .finally(() => location.reload()); - } - - async function saveCompChanges() { - // Check if tbaId is valid - if (!comp?.tbaId || !comp?.name || !comp?._id) return; - - let tbaId = newCompTbaId; - const autoFillData = await api.competitionAutofill(tbaId ?? ""); - if ( - !autoFillData?.name && - !confirm(`Invalid TBA ID: ${tbaId}. Save changes anyway?`) - ) - return; - - await api.updateCompNameAndTbaId( - comp?._id, - newCompName ?? "Unnamed", - tbaId ?? NotLinkedToTba, - ); - location.reload(); - } - const allianceIndices: number[] = []; for (let i = 0; i < games[comp?.gameId ?? defaultGameId].allianceSize; i++) { allianceIndices.push(i); @@ -492,37 +412,21 @@ export default function CompetitionIndex({
Date: Tue, 17 Dec 2024 17:50:27 -0500 Subject: [PATCH 02/69] Componentalized the rest of InsightsAndSettingsCard --- .../competition/InsightsAndSettingsCard.tsx | 21 ++++++++++--------- .../[seasonSlug]/[competitonSlug]/index.tsx | 13 ------------ 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/components/competition/InsightsAndSettingsCard.tsx b/components/competition/InsightsAndSettingsCard.tsx index 03375467..a99032e2 100644 --- a/components/competition/InsightsAndSettingsCard.tsx +++ b/components/competition/InsightsAndSettingsCard.tsx @@ -21,10 +21,6 @@ export default function InsightsAndSettingsCard(props: { toggleShowSubmittedMatches: () => void; assigningMatches: boolean; regeneratePitReports: () => void; - redAlliance: number[]; - setRedAlliance: (value: number[]) => void; - matchNumber: number | undefined; - setMatchNumber: (value: number) => void; submittedReports: number | undefined; reports: Report[]; loadingScoutStats: boolean; @@ -32,7 +28,6 @@ export default function InsightsAndSettingsCard(props: { submittedPitreports: number | undefined; seasonSlug: string | undefined; team: Team | undefined; - allianceIndices: number[]; }) { const [showSettings, setShowSettings] = useState(false); const { @@ -43,10 +38,6 @@ export default function InsightsAndSettingsCard(props: { showSubmittedMatches, toggleShowSubmittedMatches, assigningMatches, - redAlliance, - setRedAlliance, - matchNumber, - setMatchNumber, submittedReports, reports, loadingScoutStats, @@ -54,13 +45,14 @@ export default function InsightsAndSettingsCard(props: { submittedPitreports, seasonSlug, team, - allianceIndices, } = props; const [newCompName, setNewCompName] = useState(comp?.name); const [newCompTbaId, setNewCompTbaId] = useState(comp?.tbaId); const [exportPending, setExportPending] = useState(false); const [teamToAdd, setTeamToAdd] = useState(0); const [blueAlliance, setBlueAlliance] = useState([]); + const [redAlliance, setRedAlliance] = useState([]); + const [matchNumber, setMatchNumber] = useState(undefined,); const exportAsCsv = async () => { setExportPending(true); @@ -100,6 +92,15 @@ export default function InsightsAndSettingsCard(props: { location.reload(); }; + const allianceIndices: number[] = []; + for ( + let i = 0; + i < games[comp?.gameId ?? defaultGameId].allianceSize; + i++ + ) { + allianceIndices.push(i); + } + async function saveCompChanges() { // Check if tbaId is valid if (!comp?.tbaId || !comp?.name || !comp?._id) return; diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx index 041cd2d1..44e68c03 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx @@ -50,10 +50,6 @@ export default function CompetitionIndex({ false; - const [matchNumber, setMatchNumber] = useState(undefined); - - const [redAlliance, setRedAlliance] = useState([]); - const [matches, setMatches] = useState([]); const [showSubmittedMatches, setShowSubmittedMatches] = useState(false); @@ -398,10 +394,6 @@ export default function CompetitionIndex({ api.remindSlack(team._id.toString(), userId); } - const allianceIndices: number[] = []; - for (let i = 0; i < games[comp?.gameId ?? defaultGameId].allianceSize; i++) { - allianceIndices.push(i); - } return ( Date: Tue, 18 Feb 2025 16:38:17 -0500 Subject: [PATCH 03/69] Don't try to overwrite team _id in createSeason --- lib/api/ClientApi.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/api/ClientApi.ts b/lib/api/ClientApi.ts index 03c2ea5e..a0dbccb9 100644 --- a/lib/api/ClientApi.ts +++ b/lib/api/ClientApi.ts @@ -315,12 +315,14 @@ export default class ClientApi extends NextApiTemplate { gameId, ), ); - team!.seasons = [...team!.seasons, String(season._id)]; + + const { _id, ...updatedTeam } = team; + updatedTeam.seasons = [...team.seasons, String(season._id)]; await db.updateObjectById( CollectionId.Teams, new ObjectId(teamId), - team!, + updatedTeam!, ); return res.status(200).send(season); From 7da72cb5ea055f8acb5d72a8e4959da44c2ae71f Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 18 Feb 2025 16:47:37 -0500 Subject: [PATCH 04/69] Fix event page error: event not found when rankings don't exist --- pages/event/[...eventName].tsx | 44 ++++++++++++---------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/pages/event/[...eventName].tsx b/pages/event/[...eventName].tsx index 0d79ef45..eab648a1 100644 --- a/pages/event/[...eventName].tsx +++ b/pages/event/[...eventName].tsx @@ -28,19 +28,11 @@ export default function PublicEvent() { useEffect(() => { const eventName = window.location.pathname.split("/event/")[1]; - if (eventData === null) { - api.initialEventData(eventName).then((data) => { - setEventData(data); - }); - setTimeout(() => { - console.log("Event not found"); - if (stateRef.current === null) { - console.log("Event is null"); - setEventData(undefined); - } - }, 10000); - } else if (teamEvents === null) { - const firstRanking = eventData?.firstRanking; + api.initialEventData(eventName).then((data) => { + console.log(data); + setEventData(data); + + const firstRanking = data?.firstRanking; firstRanking?.map(({ team_key }) => api @@ -55,8 +47,8 @@ export default function PublicEvent() { }), ), ); - } - }); + }); + }, []); // We must always have the same number of React hooks, so we generate refs even if we aren't using them yet const countdownRefs = { @@ -82,7 +74,7 @@ export default function PublicEvent() { ); } - if (eventData === undefined || eventData.firstRanking === undefined) { + if (eventData === undefined) { return ( -
- Error:{" "} - {eventData?.comp.tbaId === NotLinkedToTba - ? "Comp Not Linked to TBA" - : "Event not found"} -
+
Error: Event not found
); } - const oprs = eventData!.oprRanking.oprs; - //@ts-ignore - const first = eventData!.firstRanking; + const oprs = eventData.oprRanking?.oprs; + const firstRanking = eventData.firstRanking; const statbotics = teamEvents ?? []; const findStatboticsStats = (key: string) => { @@ -350,15 +336,15 @@ export default function PublicEvent() {
)} - {first.length > 0 ? ( + {firstRanking?.length > 0 ? (

Ranking

- {statbotics.length < first.length && ( + {statbotics.length < firstRanking.length && ( )}
@@ -383,7 +369,7 @@ export default function PublicEvent() { - {first.map((ranking: any, index: number) => ( + {firstRanking.map((ranking: any, index: number) => ( {findStatboticsStats(ranking.team_key)?.record.qual From b4f8bcebbb9d0b90c404dd9d96319b054bc44497 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 18 Feb 2025 16:56:07 -0500 Subject: [PATCH 05/69] Add undershoot deadband for wait --- lib/client/ClientUtils.ts | 2 +- tests/lib/client/ClientUtils.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/client/ClientUtils.ts b/lib/client/ClientUtils.ts index 2eee055a..e0f85d77 100644 --- a/lib/client/ClientUtils.ts +++ b/lib/client/ClientUtils.ts @@ -144,7 +144,7 @@ export function promisify( } /** - * Tested to be accurate to within 150ms + * Tested to not go more than 150ms over the specified time and not less than 2ms under the specified time * * @tested_by tests/lib/client/ClientUtils.test.ts */ diff --git a/tests/lib/client/ClientUtils.test.ts b/tests/lib/client/ClientUtils.test.ts index e78704db..e91fdeef 100644 --- a/tests/lib/client/ClientUtils.test.ts +++ b/tests/lib/client/ClientUtils.test.ts @@ -84,7 +84,7 @@ describe(wait.name, () => { const start = Date.now(); await wait(duration); const end = Date.now(); - expect(end - start).toBeGreaterThanOrEqual(duration); + expect(end - start).toBeGreaterThanOrEqual(duration - 2); } }); From aa1de3106181eca8424b31a7aa2f6ea0d4f42ae8 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 18 Feb 2025 17:17:45 -0500 Subject: [PATCH 06/69] Fix _id overwrite error in createCompetition --- lib/api/ClientApi.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/api/ClientApi.ts b/lib/api/ClientApi.ts index a0dbccb9..c6f36f40 100644 --- a/lib/api/ClientApi.ts +++ b/lib/api/ClientApi.ts @@ -429,12 +429,14 @@ export default class ClientApi extends NextApiTemplate { ), ); - season.competitions = [...season.competitions, String(comp._id)]; + const { _id, ...updatedSeason } = season; + + updatedSeason.competitions = [...season.competitions, String(comp._id)]; await db.updateObjectById( CollectionId.Seasons, new ObjectId(season._id), - season, + updatedSeason, ); // Create reports From ab2467fed85b311ee22448f9dc583b1a710ebe59 Mon Sep 17 00:00:00 2001 From: Davis Becker <143132652+BanEvading@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:46:55 -0500 Subject: [PATCH 07/69] publishing --- components/competition/InsightsAndSettingsCard.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/components/competition/InsightsAndSettingsCard.tsx b/components/competition/InsightsAndSettingsCard.tsx index 8cd8f25b..64f96261 100644 --- a/components/competition/InsightsAndSettingsCard.tsx +++ b/components/competition/InsightsAndSettingsCard.tsx @@ -367,7 +367,7 @@ export default function InsightsAndSettingsCard(props: { ? (+( Round(submittedReports / reports.length) * 100 )).toFixed(0) - : "?"} + : "0"} %
@@ -394,7 +394,7 @@ export default function InsightsAndSettingsCard(props: { ? (+( Round(submittedReports / reports.length) * 100 )).toFixed(0) - : "?"} + : "0"} %
@@ -412,12 +412,9 @@ export default function InsightsAndSettingsCard(props: {
- {!submittedPitreports && submittedPitreports !== 0 - ? "?" - : submittedPitreports} - / + {!submittedPitreports ? "0" : submittedPitreports}/ {!pitreports || pitreports.length === 0 - ? "?" + ? "0" : pitreports.length}
From b84dba8df1cf51d7a83ede17f3d7ddccfffa5198 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Mon, 24 Feb 2025 16:49:35 -0500 Subject: [PATCH 08/69] Don't use cache in sign in flow --- lib/Auth.ts | 13 ++++++------- lib/MongoDB.ts | 14 +++++++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 4900d320..d7956ada 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -14,8 +14,6 @@ import CollectionId from "./client/CollectionId"; import { AdapterUser } from "next-auth/adapters"; import { wait } from "./client/ClientUtils"; -const db = getDatabase(); - const adapter = MongoDBAdapter(clientPromise, { databaseName: process.env.DB }); export const AuthenticationOptions: AuthOptions = { @@ -92,7 +90,7 @@ export const AuthenticationOptions: AuthOptions = { callbacks: { async session({ session, user }) { session.user = await ( - await db + await getDatabase() ).findObjectById(CollectionId.Users, new ObjectId(user.id)); return session; @@ -107,6 +105,7 @@ export const AuthenticationOptions: AuthOptions = { */ async signIn({ user }) { Analytics.signIn(user.name ?? "Unknown User"); + const db = await getDatabase(false); let typedUser = user as Partial; if (!typedUser.slug || typedUser._id?.toString() != typedUser.id) { @@ -116,9 +115,9 @@ export const AuthenticationOptions: AuthOptions = { ); let foundUser: User | undefined = undefined; while (!foundUser) { - foundUser = await ( - await db - ).findObject(CollectionId.Users, { email: typedUser.email }); + foundUser = await db.findObject(CollectionId.Users, { + email: typedUser.email, + }); if (!foundUser) await wait(50); } @@ -128,7 +127,7 @@ export const AuthenticationOptions: AuthOptions = { typedUser._id = foundUser._id; typedUser.lastSignInDateTime = new Date(); - typedUser = await repairUser(await db, typedUser); + typedUser = await repairUser(db, typedUser); console.log("User updated:", typedUser._id?.toString()); }; diff --git a/lib/MongoDB.ts b/lib/MongoDB.ts index f7343f11..f79f6362 100644 --- a/lib/MongoDB.ts +++ b/lib/MongoDB.ts @@ -30,13 +30,17 @@ clientPromise = global.clientPromise; export { clientPromise }; -export async function getDatabase(): Promise { +export async function getDatabase( + useCache: boolean = true, +): Promise { if (!global.interface) { await clientPromise; - const dbInterface = new CachedDbInterface( - new MongoDBInterface(clientPromise), - cacheOptions, - ); + + const mongo = new MongoDBInterface(clientPromise); + + const dbInterface = useCache + ? new CachedDbInterface(mongo, cacheOptions) + : mongo; await dbInterface.init(); global.interface = dbInterface; From acde19cd0a667e4d12009f24b2f35546a117224d Mon Sep 17 00:00:00 2001 From: Gearbox Bot Date: Mon, 24 Feb 2025 21:52:52 +0000 Subject: [PATCH 09/69] 1.2.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1eeff82a..677158b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sj3", - "version": "1.2.2", + "version": "1.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.2.2", + "version": "1.2.3", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", diff --git a/package.json b/package.json index cb47b22d..558cd6bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.2", + "version": "1.2.3", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", From 790097afb63e767044ccf666c41e5da702820451 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Mon, 24 Feb 2025 16:52:59 -0500 Subject: [PATCH 10/69] Reuse db in auth flow --- lib/Auth.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index d7956ada..c08923cf 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -141,15 +141,13 @@ export const AuthenticationOptions: AuthOptions = { today.toDateString() ) { // We use user.id since user._id strangely doesn't exist on user. - await getDatabase().then((db) => db.updateObjectById( CollectionId.Users, new ObjectId(typedUser._id?.toString()), { lastSignInDateTime: today, }, - ), - ); + ); } new ResendUtils().createContact(typedUser as User); From b78ba521b3a5d7565e6c77c09f1dcf28a599e9df Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Mon, 24 Feb 2025 16:54:48 -0500 Subject: [PATCH 11/69] Reduce getDatabase calls in Auth --- lib/Auth.ts | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index c08923cf..bee2f0b0 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -16,6 +16,8 @@ import { wait } from "./client/ClientUtils"; const adapter = MongoDBAdapter(clientPromise, { databaseName: process.env.DB }); +const cachedDb = getDatabase(); + export const AuthenticationOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, providers: [ @@ -28,11 +30,7 @@ export const AuthenticationOptions: AuthOptions = { profile.email, profile.picture, false, - await GenerateSlug( - await getDatabase(), - CollectionId.Users, - profile.name, - ), + await GenerateSlug(await cachedDb, CollectionId.Users, profile.name), [], [], ); @@ -60,11 +58,7 @@ export const AuthenticationOptions: AuthOptions = { profile.email, profile.picture, false, - await GenerateSlug( - await getDatabase(), - CollectionId.Users, - profile.name, - ), + await GenerateSlug(await cachedDb, CollectionId.Users, profile.name), [], [], profile.sub, @@ -90,7 +84,7 @@ export const AuthenticationOptions: AuthOptions = { callbacks: { async session({ session, user }) { session.user = await ( - await getDatabase() + await cachedDb ).findObjectById(CollectionId.Users, new ObjectId(user.id)); return session; @@ -141,13 +135,13 @@ export const AuthenticationOptions: AuthOptions = { today.toDateString() ) { // We use user.id since user._id strangely doesn't exist on user. - db.updateObjectById( - CollectionId.Users, - new ObjectId(typedUser._id?.toString()), - { - lastSignInDateTime: today, - }, - ); + db.updateObjectById( + CollectionId.Users, + new ObjectId(typedUser._id?.toString()), + { + lastSignInDateTime: today, + }, + ); } new ResendUtils().createContact(typedUser as User); From 7b9d17f51b4b765027b28fbe7678d907ce09ccdd Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Mon, 24 Feb 2025 16:56:14 -0500 Subject: [PATCH 12/69] Add log for sign in callback --- lib/Auth.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Auth.ts b/lib/Auth.ts index bee2f0b0..4967d175 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -98,6 +98,8 @@ export const AuthenticationOptions: AuthOptions = { * For email sign in, runs when the "Sign In" button is clicked (before email is sent). */ async signIn({ user }) { + console.log(`User is signing in: ${user.name}, ${user.email}, ${user.id}`); + Analytics.signIn(user.name ?? "Unknown User"); const db = await getDatabase(false); From f4d1995359bdf0c96fe5f49d2065132d4b24a9b1 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Mon, 24 Feb 2025 16:58:06 -0500 Subject: [PATCH 13/69] Format Auth --- lib/Auth.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 4967d175..d7df0093 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -98,7 +98,9 @@ export const AuthenticationOptions: AuthOptions = { * For email sign in, runs when the "Sign In" button is clicked (before email is sent). */ async signIn({ user }) { - console.log(`User is signing in: ${user.name}, ${user.email}, ${user.id}`); + console.log( + `User is signing in: ${user.name}, ${user.email}, ${user.id}`, + ); Analytics.signIn(user.name ?? "Unknown User"); const db = await getDatabase(false); From 76890ba068340c9136fbeb3b54e1316db5d2a0b6 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 16:57:45 -0500 Subject: [PATCH 14/69] Create DbInterfaceAuthAdapter --- lib/Auth.ts | 4 +- lib/DbInterfaceAuthAdapter.ts | 273 ++++++++++++++++++++++++++++++++++ lib/Types.ts | 3 + lib/client/CollectionId.ts | 27 ++-- 4 files changed, 294 insertions(+), 13 deletions(-) create mode 100644 lib/DbInterfaceAuthAdapter.ts diff --git a/lib/Auth.ts b/lib/Auth.ts index d7df0093..fecdb1b4 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -13,10 +13,10 @@ import ResendUtils from "./ResendUtils"; import CollectionId from "./client/CollectionId"; import { AdapterUser } from "next-auth/adapters"; import { wait } from "./client/ClientUtils"; - -const adapter = MongoDBAdapter(clientPromise, { databaseName: process.env.DB }); +import MongoAuthAdapter from "./DbInterfaceAuthAdapter"; const cachedDb = getDatabase(); +const adapter = MongoAuthAdapter(cachedDb); export const AuthenticationOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts new file mode 100644 index 00000000..54e3e54f --- /dev/null +++ b/lib/DbInterfaceAuthAdapter.ts @@ -0,0 +1,273 @@ +import { format, MongoDBAdapter } from "@next-auth/mongodb-adapter"; +import { + Adapter, + AdapterAccount, + AdapterSession, + AdapterUser, + VerificationToken, +} from "next-auth/adapters"; +import DbInterface from "./client/dbinterfaces/DbInterface"; +import CollectionId from "./client/CollectionId"; +import { User, Session } from "./Types"; +import { GenerateSlug } from "./Utils"; +import { ObjectId } from "bson"; + +export default function DbInterfaceAuthAdapter( + dbPromise: Promise, +): Adapter { + const base = MongoDBAdapter(clientPromise, { databaseName: process.env.DB }); + + const overrides: Adapter = { + createUser: async (data: Record) => { + const db = await dbPromise; + + const adapterUser = format.to(data); + + const user = new User( + adapterUser.name ?? "Unknown", + adapterUser.email, + adapterUser.image ?? process.env.DEFAULT_IMAGE, + false, + await GenerateSlug( + db, + CollectionId.Users, + adapterUser.name ?? "Unknown", + ), + [], + [], + undefined, + 0, + 1, + ); + + user._id = new ObjectId(adapterUser._id) as any; + + await db.addObject(CollectionId.Users, user); + return format.from(adapterUser); + }, + getUser: async (id: string) => { + const db = await dbPromise; + + if (id.length !== 24) return null; + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(id), + ); + + if (!user) return null; + return format.from(user); + }, + getUserByEmail: async (email: string) => { + const db = await dbPromise; + + const account = await db.findObject(CollectionId.Users, { email }); + + if (!account) return null; + return format.from(account); + }, + getUserByAccount: async ( + providerAccountId: Pick, + ) => { + const db = await dbPromise; + + const account = await db.findObject(CollectionId.Accounts, { + providerAccountId: providerAccountId.providerAccountId, + }); + + if (!account) return null; + + const user = await db.findObjectById( + CollectionId.Users, + account.userId as any as ObjectId, + ); + + if (!user) return null; + return format.from(user); + }, + updateUser: async ( + data: Partial & Pick, + ) => { + const db = await dbPromise; + const { _id, ...user } = format.to(data); + + const existing = await db.findObjectById( + CollectionId.Users, + new ObjectId(_id), + ); + + const result = await db.updateObjectById( + CollectionId.Users, + new ObjectId(_id), + user as Partial, + ); + + return format.from({ ...existing, ...user, _id: _id }); + }, + deleteUser: async (id: string) => { + const db = await dbPromise; + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(id), + ); + if (!user) return null; + + const account = await db.findObject(CollectionId.Accounts, { + userId: user._id, + }); + + const session = await db.findObject(CollectionId.Sessions, { + userId: user._id, + }); + + const promises = [ + db.deleteObjectById(CollectionId.Users, new ObjectId(id)), + ]; + + if (account) { + promises.push( + db.deleteObjectById(CollectionId.Accounts, new ObjectId(account._id)), + ); + } + + if (session) { + promises.push( + db.deleteObjectById(CollectionId.Sessions, new ObjectId(session._id)), + ); + } + + await Promise.all(promises); + + return format.from(user); + }, + linkAccount: async (data: Record) => { + const db = await dbPromise; + const account = format.to(data); + + await db.addObject(CollectionId.Accounts, account); + + return account; + }, + unlinkAccount: async ( + providerAccountId: Pick, + ) => { + const db = await dbPromise; + + const account = await db.findObject(CollectionId.Accounts, { + providerAccountId: providerAccountId.providerAccountId, + }); + + if (!account) return null; + + await db.deleteObjectById( + CollectionId.Accounts, + new ObjectId(account._id), + ); + + return format.from(account); + }, + getSessionAndUser: async (sessionToken: string) => { + const db = await dbPromise; + + const session = await db.findObject(CollectionId.Sessions, { + sessionToken, + }); + + if (!session) return null; + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(session.userId), + ); + + if (!user) return null; + return { + session: format.from(session), + user: format.from(user), + }; + }, + createSession: async (data: Record) => { + const db = await dbPromise; + + const session = format.to(data); + session.userId = new ObjectId(session.userId) as any; + + await db.addObject(CollectionId.Sessions, session as unknown as Session); + + return format.from(session); + }, + updateSession: async ( + data: Partial & Pick, + ) => { + const db = await dbPromise; + const { _id, ...session } = format.to(data); + + const existing = await db.findObject(CollectionId.Sessions, { + sessionToken: session.sessionToken, + }); + + if (!existing) return null; + + if (session.userId) { + session.userId = new ObjectId(session.userId) as any; + } + + const result = await db.updateObjectById( + CollectionId.Sessions, + new ObjectId(existing._id), + session as unknown as Partial, + ); + + return format.from({ ...existing, ...data }); + }, + deleteSession: async (sessionToken: string) => { + const db = await dbPromise; + + const session = await db.findObject(CollectionId.Sessions, { + sessionToken, + }); + + if (!session) return null; + + await db.deleteObjectById( + CollectionId.Sessions, + new ObjectId(session._id), + ); + + return format.from(session); + }, + createVerificationToken: async (token: VerificationToken) => { + const db = await dbPromise; + await db.addObject( + CollectionId.VerificationTokens, + format.to(token) as VerificationToken, + ); + return token; + }, + useVerificationToken: async (token: { + identifier: string; + token: string; + }) => { + const db = await dbPromise; + + const existing = await db.findObject(CollectionId.VerificationTokens, { + token: token.token, + }); + + if (!existing) return null; + + await db.deleteObjectById( + CollectionId.VerificationTokens, + new ObjectId(existing._id), + ); + + return format.from(existing); + }, + }; + + return { + ...base, + ...overrides, + }; +} diff --git a/lib/Types.ts b/lib/Types.ts index 52539d0a..d006e7f0 100644 --- a/lib/Types.ts +++ b/lib/Types.ts @@ -30,6 +30,9 @@ export interface Account extends NextAuthAccount { export interface Session extends NextAuthSession { _id: string; + sessionToken: string; + userId: ObjectId; + expires: string; } export class User implements NextAuthUser { diff --git a/lib/client/CollectionId.ts b/lib/client/CollectionId.ts index 5eb209a3..00c0c646 100644 --- a/lib/client/CollectionId.ts +++ b/lib/client/CollectionId.ts @@ -1,3 +1,4 @@ +import { VerificationToken } from "next-auth/adapters"; import { Season, Competition, @@ -12,6 +13,7 @@ import { CompPicklistGroup, WebhookHolder, } from "../Types"; +import { ObjectId } from "bson"; enum CollectionId { Seasons = "Seasons", @@ -22,6 +24,7 @@ enum CollectionId { Users = "users", Accounts = "accounts", Sessions = "sessions", + VerificationTokens = "verification_tokens", Forms = "Forms", PitReports = "Pitreports", Picklists = "Picklists", @@ -51,14 +54,16 @@ export type CollectionIdToType = ? Account : Id extends CollectionId.Sessions ? Session - : Id extends CollectionId.PitReports - ? Pitreport - : Id extends CollectionId.Picklists - ? CompPicklistGroup - : Id extends CollectionId.SubjectiveReports - ? SubjectiveReport - : Id extends CollectionId.Webhooks - ? WebhookHolder - : Id extends CollectionId.Misc - ? any - : any; + : Id extends CollectionId.VerificationTokens + ? VerificationToken & { _id: ObjectId } + : Id extends CollectionId.PitReports + ? Pitreport + : Id extends CollectionId.Picklists + ? CompPicklistGroup + : Id extends CollectionId.SubjectiveReports + ? SubjectiveReport + : Id extends CollectionId.Webhooks + ? WebhookHolder + : Id extends CollectionId.Misc + ? any + : any; From 5a4fc836389c43ac7da3101aa05fdeb30d889809 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 17:00:08 -0500 Subject: [PATCH 15/69] Remove unneeded DB call in session callback --- lib/Auth.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index fecdb1b4..f42bfb0c 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -83,9 +83,7 @@ export const AuthenticationOptions: AuthOptions = { ], callbacks: { async session({ session, user }) { - session.user = await ( - await cachedDb - ).findObjectById(CollectionId.Users, new ObjectId(user.id)); + session.user = user; return session; }, From 1803cd1611c8e27b5b97c8edd71cfa00ef3f1ca4 Mon Sep 17 00:00:00 2001 From: Gearbox Bot Date: Tue, 25 Feb 2025 22:00:35 +0000 Subject: [PATCH 16/69] 1.2.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 677158b3..daa00cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sj3", - "version": "1.2.3", + "version": "1.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.2.3", + "version": "1.2.4", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", diff --git a/package.json b/package.json index 558cd6bb..308a4c55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.3", + "version": "1.2.4", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", From ed77b0b2ef3b77b07e129d4ea7951392d234bbb7 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 17:02:40 -0500 Subject: [PATCH 17/69] Remove unneeded variable --- lib/DbInterfaceAuthAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index 54e3e54f..77c90d50 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -213,7 +213,7 @@ export default function DbInterfaceAuthAdapter( session.userId = new ObjectId(session.userId) as any; } - const result = await db.updateObjectById( + await db.updateObjectById( CollectionId.Sessions, new ObjectId(existing._id), session as unknown as Partial, From f6f2247945e3a4b2d5dda4d808ce07a9cac6f256 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 17:11:52 -0500 Subject: [PATCH 18/69] Remove base adapter --- lib/DbInterfaceAuthAdapter.ts | 9 ++---- tests/lib/DbInterfaceAuthAdapter.test.ts | 38 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 tests/lib/DbInterfaceAuthAdapter.test.ts diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index 77c90d50..56372868 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -15,9 +15,7 @@ import { ObjectId } from "bson"; export default function DbInterfaceAuthAdapter( dbPromise: Promise, ): Adapter { - const base = MongoDBAdapter(clientPromise, { databaseName: process.env.DB }); - - const overrides: Adapter = { + const adapter: Adapter = { createUser: async (data: Record) => { const db = await dbPromise; @@ -266,8 +264,5 @@ export default function DbInterfaceAuthAdapter( }, }; - return { - ...base, - ...overrides, - }; + return adapter; } diff --git a/tests/lib/DbInterfaceAuthAdapter.test.ts b/tests/lib/DbInterfaceAuthAdapter.test.ts new file mode 100644 index 00000000..4e2e0262 --- /dev/null +++ b/tests/lib/DbInterfaceAuthAdapter.test.ts @@ -0,0 +1,38 @@ +import CollectionId from "@/lib/client/CollectionId"; +import InMemoryDbInterface from "@/lib/client/dbinterfaces/InMemoryDbInterface"; +import DbInterfaceAuthAdapter from "@/lib/DbInterfaceAuthAdapter"; +import { get } from "http"; + +const prototype = DbInterfaceAuthAdapter(undefined as any); + +async function getDatabase() {} + +async function getAdapterAndDb() { + const db = new InMemoryDbInterface(); + await db.init(); + + return { + adapter: DbInterfaceAuthAdapter(Promise.resolve(db)), + db, + }; +} + +describe(prototype.createUser.name, () => { + test("Adds a user to the database", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + name: "Test User", + email: "test@gmail.com", + image: "test.png", + }; + + await adapter.createUser(user); + + const foundUser = await db.findObject(CollectionId.Users, { + email: user.email, + }); + + expect(foundUser).toMatchObject(user); + }); +}); From 58252d469fb43c2f1aa8893f01650d553782812a Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 17:32:26 -0500 Subject: [PATCH 19/69] More tests and logs --- .env.test | 4 +- lib/DbInterfaceAuthAdapter.ts | 38 +++++++++++ tests/lib/DbInterfaceAuthAdapter.test.ts | 82 ++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/.env.test b/.env.test index 67bb80a9..3eae1f85 100644 --- a/.env.test +++ b/.env.test @@ -4,4 +4,6 @@ DEVELOPER_EMAILS=["test@gmail.com"] TOA_URL=https://example.com TOA_APP_ID=123 -TOA_KEY=456 \ No newline at end of file +TOA_KEY=456 + +DEFAULT_IMAGE=https://example.com/default.jpg \ No newline at end of file diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index 56372868..be3e3dac 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -21,6 +21,8 @@ export default function DbInterfaceAuthAdapter( const adapterUser = format.to(data); + console.log("Creating user:", adapterUser.name); + const user = new User( adapterUser.name ?? "Unknown", adapterUser.email, @@ -48,6 +50,8 @@ export default function DbInterfaceAuthAdapter( if (id.length !== 24) return null; + console.log("Getting user:", id); + const user = await db.findObjectById( CollectionId.Users, new ObjectId(id), @@ -59,6 +63,8 @@ export default function DbInterfaceAuthAdapter( getUserByEmail: async (email: string) => { const db = await dbPromise; + console.log("Getting user by email:", email); + const account = await db.findObject(CollectionId.Users, { email }); if (!account) return null; @@ -69,6 +75,11 @@ export default function DbInterfaceAuthAdapter( ) => { const db = await dbPromise; + console.log( + "Getting user by account:", + providerAccountId.providerAccountId, + ); + const account = await db.findObject(CollectionId.Accounts, { providerAccountId: providerAccountId.providerAccountId, }); @@ -89,6 +100,8 @@ export default function DbInterfaceAuthAdapter( const db = await dbPromise; const { _id, ...user } = format.to(data); + console.log("Updating user:", _id); + const existing = await db.findObjectById( CollectionId.Users, new ObjectId(_id), @@ -105,6 +118,8 @@ export default function DbInterfaceAuthAdapter( deleteUser: async (id: string) => { const db = await dbPromise; + console.log("Deleting user:", id); + const user = await db.findObjectById( CollectionId.Users, new ObjectId(id), @@ -143,6 +158,13 @@ export default function DbInterfaceAuthAdapter( const db = await dbPromise; const account = format.to(data); + console.log( + "Linking account:", + account.providerAccountId, + "User:", + account.userId, + ); + await db.addObject(CollectionId.Accounts, account); return account; @@ -152,6 +174,8 @@ export default function DbInterfaceAuthAdapter( ) => { const db = await dbPromise; + console.log("Unlinking account:", providerAccountId.providerAccountId); + const account = await db.findObject(CollectionId.Accounts, { providerAccountId: providerAccountId.providerAccountId, }); @@ -168,6 +192,8 @@ export default function DbInterfaceAuthAdapter( getSessionAndUser: async (sessionToken: string) => { const db = await dbPromise; + console.log("Getting session and user:", sessionToken); + const session = await db.findObject(CollectionId.Sessions, { sessionToken, }); @@ -189,6 +215,9 @@ export default function DbInterfaceAuthAdapter( const db = await dbPromise; const session = format.to(data); + + console.log("Creating session:", session.sessionToken); + session.userId = new ObjectId(session.userId) as any; await db.addObject(CollectionId.Sessions, session as unknown as Session); @@ -201,6 +230,8 @@ export default function DbInterfaceAuthAdapter( const db = await dbPromise; const { _id, ...session } = format.to(data); + console.log("Updating session:", session.sessionToken); + const existing = await db.findObject(CollectionId.Sessions, { sessionToken: session.sessionToken, }); @@ -222,6 +253,8 @@ export default function DbInterfaceAuthAdapter( deleteSession: async (sessionToken: string) => { const db = await dbPromise; + console.log("Deleting session:", sessionToken); + const session = await db.findObject(CollectionId.Sessions, { sessionToken, }); @@ -237,6 +270,9 @@ export default function DbInterfaceAuthAdapter( }, createVerificationToken: async (token: VerificationToken) => { const db = await dbPromise; + + console.log("Creating verification token:", token.identifier); + await db.addObject( CollectionId.VerificationTokens, format.to(token) as VerificationToken, @@ -249,6 +285,8 @@ export default function DbInterfaceAuthAdapter( }) => { const db = await dbPromise; + console.log("Using verification token:", token.identifier); + const existing = await db.findObject(CollectionId.VerificationTokens, { token: token.token, }); diff --git a/tests/lib/DbInterfaceAuthAdapter.test.ts b/tests/lib/DbInterfaceAuthAdapter.test.ts index 4e2e0262..a76bef4d 100644 --- a/tests/lib/DbInterfaceAuthAdapter.test.ts +++ b/tests/lib/DbInterfaceAuthAdapter.test.ts @@ -1,6 +1,8 @@ import CollectionId from "@/lib/client/CollectionId"; import InMemoryDbInterface from "@/lib/client/dbinterfaces/InMemoryDbInterface"; import DbInterfaceAuthAdapter from "@/lib/DbInterfaceAuthAdapter"; +import { _id } from "@next-auth/mongodb-adapter"; +import { ObjectId } from "bson"; import { get } from "http"; const prototype = DbInterfaceAuthAdapter(undefined as any); @@ -35,4 +37,84 @@ describe(prototype.createUser.name, () => { expect(foundUser).toMatchObject(user); }); + + test("Populates fields with default values", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + name: "Test User", + email: "test@gmail.com", + image: "test.png", + }; + + await adapter.createUser(user); + + const foundUser = await db.findObject(CollectionId.Users, { + email: user.email, + }); + + expect(foundUser?.name).toBeDefined(); + expect(foundUser?.email).toBeDefined(); + expect(foundUser?.image).toBeDefined; + expect(foundUser?.admin).toBeDefined(); + expect(foundUser?.slug).toBeDefined(); + expect(foundUser?.teams).toBeDefined(); + expect(foundUser?.owner).toBeDefined(); + expect(foundUser?.level).toBeDefined(); + expect(foundUser?.xp).toBeDefined(); + }); + + test("Populates missing fields with defaults", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + email: "test@gmail.com", + }; + + await adapter.createUser(user); + + const foundUser = await db.findObject(CollectionId.Users, { + email: user.email, + }); + + expect(foundUser?.name).toBeDefined(); + expect(foundUser?.image).toBeDefined(); + }); +}); + +describe(prototype.getUser!.name, () => { + test("Returns a user from the database without their _id", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + _id: new ObjectId(), + name: "Test User", + email: "test@gmail.com", + image: "test.png", + }; + + await db.addObject(CollectionId.Users, user as any); + + const foundUser = await adapter.getUser!(user._id.toString()); + + const { _id, ...userWithoutId } = user; + + expect(foundUser).toMatchObject(userWithoutId); + }); + + test("Returns null if given an id of the wrong length", async () => { + const { adapter } = await getAdapterAndDb(); + + const foundUser = await adapter.getUser!("1234567890123456789012345"); + + expect(foundUser).toBeNull(); + }); + + test("Returns null if the user doesn't exist", async () => { + const { adapter } = await getAdapterAndDb(); + + const foundUser = await adapter.getUser!(new ObjectId().toString()); + + expect(foundUser).toBeNull(); + }); }); From c32993a00299a657ad83e24e833acfd3bec8fc30 Mon Sep 17 00:00:00 2001 From: Gearbox Bot Date: Tue, 25 Feb 2025 22:33:14 +0000 Subject: [PATCH 20/69] 1.2.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index daa00cbb..346c0995 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sj3", - "version": "1.2.4", + "version": "1.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.2.4", + "version": "1.2.5", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", diff --git a/package.json b/package.json index 308a4c55..d6f4a5de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.4", + "version": "1.2.5", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", From 58f8273e0d7462a880c2e4a8fd0b5448cfe6ce49 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 17:48:05 -0500 Subject: [PATCH 21/69] More logs --- lib/DbInterfaceAuthAdapter.ts | 306 ++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 lib/DbInterfaceAuthAdapter.ts diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts new file mode 100644 index 00000000..fe11ce36 --- /dev/null +++ b/lib/DbInterfaceAuthAdapter.ts @@ -0,0 +1,306 @@ +import { format, MongoDBAdapter } from "@next-auth/mongodb-adapter"; +import { + Adapter, + AdapterAccount, + AdapterSession, + AdapterUser, + VerificationToken, +} from "next-auth/adapters"; +import DbInterface from "./client/dbinterfaces/DbInterface"; +import CollectionId from "./client/CollectionId"; +import { User, Session } from "./Types"; +import { GenerateSlug } from "./Utils"; +import { ObjectId } from "bson"; + +export default function DbInterfaceAuthAdapter( + dbPromise: Promise, +): Adapter { + const adapter: Adapter = { + createUser: async (data: Record) => { + const db = await dbPromise; + + const adapterUser = format.to(data); + + console.log("[AUTH] Creating user:", adapterUser.name); + + const user = new User( + adapterUser.name ?? "Unknown", + adapterUser.email, + adapterUser.image ?? process.env.DEFAULT_IMAGE, + false, + await GenerateSlug( + db, + CollectionId.Users, + adapterUser.name ?? "Unknown", + ), + [], + [], + undefined, + 0, + 1, + ); + + user._id = new ObjectId(adapterUser._id) as any; + + await db.addObject(CollectionId.Users, user); + return format.from(adapterUser); + }, + getUser: async (id: string) => { + const db = await dbPromise; + + if (id.length !== 24) return null; + + console.log("[AUTH] Getting user:", id); + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(id), + ); + + if (!user) return null; + return format.from(user); + }, + getUserByEmail: async (email: string) => { + const db = await dbPromise; + + console.log("[AUTH] Getting user by email:", email); + + const account = await db.findObject(CollectionId.Users, { email }); + + if (!account) return null; + return format.from(account); + }, + getUserByAccount: async ( + providerAccountId: Pick, + ) => { + const db = await dbPromise; + + console.log( + "Getting user by account:", + providerAccountId.providerAccountId, + ); + + const account = await db.findObject(CollectionId.Accounts, { + providerAccountId: providerAccountId.providerAccountId, + }); + + if (!account) return null; + + const user = await db.findObjectById( + CollectionId.Users, + account.userId as any as ObjectId, + ); + + if (!user) return null; + return format.from(user); + }, + updateUser: async ( + data: Partial & Pick, + ) => { + const db = await dbPromise; + const { _id, ...user } = format.to(data); + + console.log("[AUTH] Updating user:", _id); + + const existing = await db.findObjectById( + CollectionId.Users, + new ObjectId(_id), + ); + + const result = await db.updateObjectById( + CollectionId.Users, + new ObjectId(_id), + user as Partial, + ); + + return format.from({ ...existing, ...user, _id: _id }); + }, + deleteUser: async (id: string) => { + const db = await dbPromise; + + console.log("[AUTH] Deleting user:", id); + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(id), + ); + if (!user) return null; + + const account = await db.findObject(CollectionId.Accounts, { + userId: user._id, + }); + + const session = await db.findObject(CollectionId.Sessions, { + userId: user._id, + }); + + const promises = [ + db.deleteObjectById(CollectionId.Users, new ObjectId(id)), + ]; + + if (account) { + promises.push( + db.deleteObjectById(CollectionId.Accounts, new ObjectId(account._id)), + ); + } + + if (session) { + promises.push( + db.deleteObjectById(CollectionId.Sessions, new ObjectId(session._id)), + ); + } + + await Promise.all(promises); + + return format.from(user); + }, + linkAccount: async (data: Record) => { + const db = await dbPromise; + const account = format.to(data); + + console.log( + "Linking account:", + account.providerAccountId, + "User:", + account.userId, + ); + + await db.addObject(CollectionId.Accounts, account); + + return account; + }, + unlinkAccount: async ( + providerAccountId: Pick, + ) => { + const db = await dbPromise; + + console.log("[AUTH] Unlinking account:", providerAccountId.providerAccountId); + + const account = await db.findObject(CollectionId.Accounts, { + providerAccountId: providerAccountId.providerAccountId, + }); + + if (!account) return null; + + await db.deleteObjectById( + CollectionId.Accounts, + new ObjectId(account._id), + ); + + return format.from(account); + }, + getSessionAndUser: async (sessionToken: string) => { + const db = await dbPromise; + + console.log("[AUTH] Getting session and user:", sessionToken); + + const session = await db.findObject(CollectionId.Sessions, { + sessionToken, + }); + + if (!session) return null; + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(session.userId), + ); + + if (!user) return null; + return { + session: format.from(session), + user: format.from(user), + }; + }, + createSession: async (data: Record) => { + const db = await dbPromise; + + const session = format.to(data); + + console.log("[AUTH] Creating session:", session.sessionToken); + + session.userId = new ObjectId(session.userId) as any; + + await db.addObject(CollectionId.Sessions, session as unknown as Session); + + return format.from(session); + }, + updateSession: async ( + data: Partial & Pick, + ) => { + const db = await dbPromise; + const { _id, ...session } = format.to(data); + + console.log("[AUTH] Updating session:", session.sessionToken); + + const existing = await db.findObject(CollectionId.Sessions, { + sessionToken: session.sessionToken, + }); + + if (!existing) return null; + + if (session.userId) { + session.userId = new ObjectId(session.userId) as any; + } + + await db.updateObjectById( + CollectionId.Sessions, + new ObjectId(existing._id), + session as unknown as Partial, + ); + + return format.from({ ...existing, ...data }); + }, + deleteSession: async (sessionToken: string) => { + const db = await dbPromise; + + console.log("[AUTH] Deleting session:", sessionToken); + + const session = await db.findObject(CollectionId.Sessions, { + sessionToken, + }); + + if (!session) return null; + + await db.deleteObjectById( + CollectionId.Sessions, + new ObjectId(session._id), + ); + + return format.from(session); + }, + createVerificationToken: async (token: VerificationToken) => { + const db = await dbPromise; + + console.log("[AUTH] Creating verification token:", token.identifier); + + await db.addObject( + CollectionId.VerificationTokens, + format.to(token) as VerificationToken, + ); + return token; + }, + useVerificationToken: async (token: { + identifier: string; + token: string; + }) => { + const db = await dbPromise; + + console.log("[AUTH] Using verification token:", token.identifier); + + const existing = await db.findObject(CollectionId.VerificationTokens, { + token: token.token, + }); + + if (!existing) return null; + + await db.deleteObjectById( + CollectionId.VerificationTokens, + new ObjectId(existing._id), + ); + + return format.from(existing); + }, + }; + + return adapter; +} From 392d3ddfa5fc138e92b430cd9cd007389c2f2ea7 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 17:52:02 -0500 Subject: [PATCH 22/69] Fix formatting in adapter --- lib/DbInterfaceAuthAdapter.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index fe11ce36..89ead64e 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -174,7 +174,10 @@ export default function DbInterfaceAuthAdapter( ) => { const db = await dbPromise; - console.log("[AUTH] Unlinking account:", providerAccountId.providerAccountId); + console.log( + "[AUTH] Unlinking account:", + providerAccountId.providerAccountId, + ); const account = await db.findObject(CollectionId.Accounts, { providerAccountId: providerAccountId.providerAccountId, From 7fc9ffcbca5452dc60f6fbe72a4bcc9ba520c5a6 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 17:59:38 -0500 Subject: [PATCH 23/69] Revert "Fix formatting in adapter" This reverts commit 392d3ddfa5fc138e92b430cd9cd007389c2f2ea7. --- lib/DbInterfaceAuthAdapter.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index 89ead64e..fe11ce36 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -174,10 +174,7 @@ export default function DbInterfaceAuthAdapter( ) => { const db = await dbPromise; - console.log( - "[AUTH] Unlinking account:", - providerAccountId.providerAccountId, - ); + console.log("[AUTH] Unlinking account:", providerAccountId.providerAccountId); const account = await db.findObject(CollectionId.Accounts, { providerAccountId: providerAccountId.providerAccountId, From bdad8f389c63cf171c3f763840c23e20df74f95a Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 18:01:59 -0500 Subject: [PATCH 24/69] Revert "Merge branch 'main' of github.com:Decatur-Robotics/Gearbox" This reverts commit b919af71f9bc1de9be1666b626d6b09d2d684b51, reversing changes made to 58f8273e0d7462a880c2e4a8fd0b5448cfe6ce49. --- .env.test | 4 +- lib/Auth.ts | 8 +- lib/Types.ts | 3 - lib/client/CollectionId.ts | 27 +++-- package-lock.json | 4 +- package.json | 2 +- tests/lib/DbInterfaceAuthAdapter.test.ts | 120 ----------------------- 7 files changed, 20 insertions(+), 148 deletions(-) delete mode 100644 tests/lib/DbInterfaceAuthAdapter.test.ts diff --git a/.env.test b/.env.test index 3eae1f85..67bb80a9 100644 --- a/.env.test +++ b/.env.test @@ -4,6 +4,4 @@ DEVELOPER_EMAILS=["test@gmail.com"] TOA_URL=https://example.com TOA_APP_ID=123 -TOA_KEY=456 - -DEFAULT_IMAGE=https://example.com/default.jpg \ No newline at end of file +TOA_KEY=456 \ No newline at end of file diff --git a/lib/Auth.ts b/lib/Auth.ts index f42bfb0c..d7df0093 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -13,10 +13,10 @@ import ResendUtils from "./ResendUtils"; import CollectionId from "./client/CollectionId"; import { AdapterUser } from "next-auth/adapters"; import { wait } from "./client/ClientUtils"; -import MongoAuthAdapter from "./DbInterfaceAuthAdapter"; + +const adapter = MongoDBAdapter(clientPromise, { databaseName: process.env.DB }); const cachedDb = getDatabase(); -const adapter = MongoAuthAdapter(cachedDb); export const AuthenticationOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, @@ -83,7 +83,9 @@ export const AuthenticationOptions: AuthOptions = { ], callbacks: { async session({ session, user }) { - session.user = user; + session.user = await ( + await cachedDb + ).findObjectById(CollectionId.Users, new ObjectId(user.id)); return session; }, diff --git a/lib/Types.ts b/lib/Types.ts index d006e7f0..52539d0a 100644 --- a/lib/Types.ts +++ b/lib/Types.ts @@ -30,9 +30,6 @@ export interface Account extends NextAuthAccount { export interface Session extends NextAuthSession { _id: string; - sessionToken: string; - userId: ObjectId; - expires: string; } export class User implements NextAuthUser { diff --git a/lib/client/CollectionId.ts b/lib/client/CollectionId.ts index 00c0c646..5eb209a3 100644 --- a/lib/client/CollectionId.ts +++ b/lib/client/CollectionId.ts @@ -1,4 +1,3 @@ -import { VerificationToken } from "next-auth/adapters"; import { Season, Competition, @@ -13,7 +12,6 @@ import { CompPicklistGroup, WebhookHolder, } from "../Types"; -import { ObjectId } from "bson"; enum CollectionId { Seasons = "Seasons", @@ -24,7 +22,6 @@ enum CollectionId { Users = "users", Accounts = "accounts", Sessions = "sessions", - VerificationTokens = "verification_tokens", Forms = "Forms", PitReports = "Pitreports", Picklists = "Picklists", @@ -54,16 +51,14 @@ export type CollectionIdToType = ? Account : Id extends CollectionId.Sessions ? Session - : Id extends CollectionId.VerificationTokens - ? VerificationToken & { _id: ObjectId } - : Id extends CollectionId.PitReports - ? Pitreport - : Id extends CollectionId.Picklists - ? CompPicklistGroup - : Id extends CollectionId.SubjectiveReports - ? SubjectiveReport - : Id extends CollectionId.Webhooks - ? WebhookHolder - : Id extends CollectionId.Misc - ? any - : any; + : Id extends CollectionId.PitReports + ? Pitreport + : Id extends CollectionId.Picklists + ? CompPicklistGroup + : Id extends CollectionId.SubjectiveReports + ? SubjectiveReport + : Id extends CollectionId.Webhooks + ? WebhookHolder + : Id extends CollectionId.Misc + ? any + : any; diff --git a/package-lock.json b/package-lock.json index 346c0995..677158b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sj3", - "version": "1.2.5", + "version": "1.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.2.5", + "version": "1.2.3", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", diff --git a/package.json b/package.json index d6f4a5de..558cd6bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.5", + "version": "1.2.3", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", diff --git a/tests/lib/DbInterfaceAuthAdapter.test.ts b/tests/lib/DbInterfaceAuthAdapter.test.ts deleted file mode 100644 index a76bef4d..00000000 --- a/tests/lib/DbInterfaceAuthAdapter.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import CollectionId from "@/lib/client/CollectionId"; -import InMemoryDbInterface from "@/lib/client/dbinterfaces/InMemoryDbInterface"; -import DbInterfaceAuthAdapter from "@/lib/DbInterfaceAuthAdapter"; -import { _id } from "@next-auth/mongodb-adapter"; -import { ObjectId } from "bson"; -import { get } from "http"; - -const prototype = DbInterfaceAuthAdapter(undefined as any); - -async function getDatabase() {} - -async function getAdapterAndDb() { - const db = new InMemoryDbInterface(); - await db.init(); - - return { - adapter: DbInterfaceAuthAdapter(Promise.resolve(db)), - db, - }; -} - -describe(prototype.createUser.name, () => { - test("Adds a user to the database", async () => { - const { db, adapter } = await getAdapterAndDb(); - - const user = { - name: "Test User", - email: "test@gmail.com", - image: "test.png", - }; - - await adapter.createUser(user); - - const foundUser = await db.findObject(CollectionId.Users, { - email: user.email, - }); - - expect(foundUser).toMatchObject(user); - }); - - test("Populates fields with default values", async () => { - const { db, adapter } = await getAdapterAndDb(); - - const user = { - name: "Test User", - email: "test@gmail.com", - image: "test.png", - }; - - await adapter.createUser(user); - - const foundUser = await db.findObject(CollectionId.Users, { - email: user.email, - }); - - expect(foundUser?.name).toBeDefined(); - expect(foundUser?.email).toBeDefined(); - expect(foundUser?.image).toBeDefined; - expect(foundUser?.admin).toBeDefined(); - expect(foundUser?.slug).toBeDefined(); - expect(foundUser?.teams).toBeDefined(); - expect(foundUser?.owner).toBeDefined(); - expect(foundUser?.level).toBeDefined(); - expect(foundUser?.xp).toBeDefined(); - }); - - test("Populates missing fields with defaults", async () => { - const { db, adapter } = await getAdapterAndDb(); - - const user = { - email: "test@gmail.com", - }; - - await adapter.createUser(user); - - const foundUser = await db.findObject(CollectionId.Users, { - email: user.email, - }); - - expect(foundUser?.name).toBeDefined(); - expect(foundUser?.image).toBeDefined(); - }); -}); - -describe(prototype.getUser!.name, () => { - test("Returns a user from the database without their _id", async () => { - const { db, adapter } = await getAdapterAndDb(); - - const user = { - _id: new ObjectId(), - name: "Test User", - email: "test@gmail.com", - image: "test.png", - }; - - await db.addObject(CollectionId.Users, user as any); - - const foundUser = await adapter.getUser!(user._id.toString()); - - const { _id, ...userWithoutId } = user; - - expect(foundUser).toMatchObject(userWithoutId); - }); - - test("Returns null if given an id of the wrong length", async () => { - const { adapter } = await getAdapterAndDb(); - - const foundUser = await adapter.getUser!("1234567890123456789012345"); - - expect(foundUser).toBeNull(); - }); - - test("Returns null if the user doesn't exist", async () => { - const { adapter } = await getAdapterAndDb(); - - const foundUser = await adapter.getUser!(new ObjectId().toString()); - - expect(foundUser).toBeNull(); - }); -}); From 00791b655fb63aaa9edc8398b79dcc8ecbbd00e6 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 18:02:05 -0500 Subject: [PATCH 25/69] Revert "More logs" This reverts commit 58f8273e0d7462a880c2e4a8fd0b5448cfe6ce49. --- lib/DbInterfaceAuthAdapter.ts | 306 ---------------------------------- 1 file changed, 306 deletions(-) delete mode 100644 lib/DbInterfaceAuthAdapter.ts diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts deleted file mode 100644 index fe11ce36..00000000 --- a/lib/DbInterfaceAuthAdapter.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { format, MongoDBAdapter } from "@next-auth/mongodb-adapter"; -import { - Adapter, - AdapterAccount, - AdapterSession, - AdapterUser, - VerificationToken, -} from "next-auth/adapters"; -import DbInterface from "./client/dbinterfaces/DbInterface"; -import CollectionId from "./client/CollectionId"; -import { User, Session } from "./Types"; -import { GenerateSlug } from "./Utils"; -import { ObjectId } from "bson"; - -export default function DbInterfaceAuthAdapter( - dbPromise: Promise, -): Adapter { - const adapter: Adapter = { - createUser: async (data: Record) => { - const db = await dbPromise; - - const adapterUser = format.to(data); - - console.log("[AUTH] Creating user:", adapterUser.name); - - const user = new User( - adapterUser.name ?? "Unknown", - adapterUser.email, - adapterUser.image ?? process.env.DEFAULT_IMAGE, - false, - await GenerateSlug( - db, - CollectionId.Users, - adapterUser.name ?? "Unknown", - ), - [], - [], - undefined, - 0, - 1, - ); - - user._id = new ObjectId(adapterUser._id) as any; - - await db.addObject(CollectionId.Users, user); - return format.from(adapterUser); - }, - getUser: async (id: string) => { - const db = await dbPromise; - - if (id.length !== 24) return null; - - console.log("[AUTH] Getting user:", id); - - const user = await db.findObjectById( - CollectionId.Users, - new ObjectId(id), - ); - - if (!user) return null; - return format.from(user); - }, - getUserByEmail: async (email: string) => { - const db = await dbPromise; - - console.log("[AUTH] Getting user by email:", email); - - const account = await db.findObject(CollectionId.Users, { email }); - - if (!account) return null; - return format.from(account); - }, - getUserByAccount: async ( - providerAccountId: Pick, - ) => { - const db = await dbPromise; - - console.log( - "Getting user by account:", - providerAccountId.providerAccountId, - ); - - const account = await db.findObject(CollectionId.Accounts, { - providerAccountId: providerAccountId.providerAccountId, - }); - - if (!account) return null; - - const user = await db.findObjectById( - CollectionId.Users, - account.userId as any as ObjectId, - ); - - if (!user) return null; - return format.from(user); - }, - updateUser: async ( - data: Partial & Pick, - ) => { - const db = await dbPromise; - const { _id, ...user } = format.to(data); - - console.log("[AUTH] Updating user:", _id); - - const existing = await db.findObjectById( - CollectionId.Users, - new ObjectId(_id), - ); - - const result = await db.updateObjectById( - CollectionId.Users, - new ObjectId(_id), - user as Partial, - ); - - return format.from({ ...existing, ...user, _id: _id }); - }, - deleteUser: async (id: string) => { - const db = await dbPromise; - - console.log("[AUTH] Deleting user:", id); - - const user = await db.findObjectById( - CollectionId.Users, - new ObjectId(id), - ); - if (!user) return null; - - const account = await db.findObject(CollectionId.Accounts, { - userId: user._id, - }); - - const session = await db.findObject(CollectionId.Sessions, { - userId: user._id, - }); - - const promises = [ - db.deleteObjectById(CollectionId.Users, new ObjectId(id)), - ]; - - if (account) { - promises.push( - db.deleteObjectById(CollectionId.Accounts, new ObjectId(account._id)), - ); - } - - if (session) { - promises.push( - db.deleteObjectById(CollectionId.Sessions, new ObjectId(session._id)), - ); - } - - await Promise.all(promises); - - return format.from(user); - }, - linkAccount: async (data: Record) => { - const db = await dbPromise; - const account = format.to(data); - - console.log( - "Linking account:", - account.providerAccountId, - "User:", - account.userId, - ); - - await db.addObject(CollectionId.Accounts, account); - - return account; - }, - unlinkAccount: async ( - providerAccountId: Pick, - ) => { - const db = await dbPromise; - - console.log("[AUTH] Unlinking account:", providerAccountId.providerAccountId); - - const account = await db.findObject(CollectionId.Accounts, { - providerAccountId: providerAccountId.providerAccountId, - }); - - if (!account) return null; - - await db.deleteObjectById( - CollectionId.Accounts, - new ObjectId(account._id), - ); - - return format.from(account); - }, - getSessionAndUser: async (sessionToken: string) => { - const db = await dbPromise; - - console.log("[AUTH] Getting session and user:", sessionToken); - - const session = await db.findObject(CollectionId.Sessions, { - sessionToken, - }); - - if (!session) return null; - - const user = await db.findObjectById( - CollectionId.Users, - new ObjectId(session.userId), - ); - - if (!user) return null; - return { - session: format.from(session), - user: format.from(user), - }; - }, - createSession: async (data: Record) => { - const db = await dbPromise; - - const session = format.to(data); - - console.log("[AUTH] Creating session:", session.sessionToken); - - session.userId = new ObjectId(session.userId) as any; - - await db.addObject(CollectionId.Sessions, session as unknown as Session); - - return format.from(session); - }, - updateSession: async ( - data: Partial & Pick, - ) => { - const db = await dbPromise; - const { _id, ...session } = format.to(data); - - console.log("[AUTH] Updating session:", session.sessionToken); - - const existing = await db.findObject(CollectionId.Sessions, { - sessionToken: session.sessionToken, - }); - - if (!existing) return null; - - if (session.userId) { - session.userId = new ObjectId(session.userId) as any; - } - - await db.updateObjectById( - CollectionId.Sessions, - new ObjectId(existing._id), - session as unknown as Partial, - ); - - return format.from({ ...existing, ...data }); - }, - deleteSession: async (sessionToken: string) => { - const db = await dbPromise; - - console.log("[AUTH] Deleting session:", sessionToken); - - const session = await db.findObject(CollectionId.Sessions, { - sessionToken, - }); - - if (!session) return null; - - await db.deleteObjectById( - CollectionId.Sessions, - new ObjectId(session._id), - ); - - return format.from(session); - }, - createVerificationToken: async (token: VerificationToken) => { - const db = await dbPromise; - - console.log("[AUTH] Creating verification token:", token.identifier); - - await db.addObject( - CollectionId.VerificationTokens, - format.to(token) as VerificationToken, - ); - return token; - }, - useVerificationToken: async (token: { - identifier: string; - token: string; - }) => { - const db = await dbPromise; - - console.log("[AUTH] Using verification token:", token.identifier); - - const existing = await db.findObject(CollectionId.VerificationTokens, { - token: token.token, - }); - - if (!existing) return null; - - await db.deleteObjectById( - CollectionId.VerificationTokens, - new ObjectId(existing._id), - ); - - return format.from(existing); - }, - }; - - return adapter; -} From e43a1d9d5a406e53b13ced1b98739d947ca0324d Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 18:03:02 -0500 Subject: [PATCH 26/69] Reapply "More logs" This reverts commit 00791b655fb63aaa9edc8398b79dcc8ecbbd00e6. --- lib/DbInterfaceAuthAdapter.ts | 306 ++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 lib/DbInterfaceAuthAdapter.ts diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts new file mode 100644 index 00000000..fe11ce36 --- /dev/null +++ b/lib/DbInterfaceAuthAdapter.ts @@ -0,0 +1,306 @@ +import { format, MongoDBAdapter } from "@next-auth/mongodb-adapter"; +import { + Adapter, + AdapterAccount, + AdapterSession, + AdapterUser, + VerificationToken, +} from "next-auth/adapters"; +import DbInterface from "./client/dbinterfaces/DbInterface"; +import CollectionId from "./client/CollectionId"; +import { User, Session } from "./Types"; +import { GenerateSlug } from "./Utils"; +import { ObjectId } from "bson"; + +export default function DbInterfaceAuthAdapter( + dbPromise: Promise, +): Adapter { + const adapter: Adapter = { + createUser: async (data: Record) => { + const db = await dbPromise; + + const adapterUser = format.to(data); + + console.log("[AUTH] Creating user:", adapterUser.name); + + const user = new User( + adapterUser.name ?? "Unknown", + adapterUser.email, + adapterUser.image ?? process.env.DEFAULT_IMAGE, + false, + await GenerateSlug( + db, + CollectionId.Users, + adapterUser.name ?? "Unknown", + ), + [], + [], + undefined, + 0, + 1, + ); + + user._id = new ObjectId(adapterUser._id) as any; + + await db.addObject(CollectionId.Users, user); + return format.from(adapterUser); + }, + getUser: async (id: string) => { + const db = await dbPromise; + + if (id.length !== 24) return null; + + console.log("[AUTH] Getting user:", id); + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(id), + ); + + if (!user) return null; + return format.from(user); + }, + getUserByEmail: async (email: string) => { + const db = await dbPromise; + + console.log("[AUTH] Getting user by email:", email); + + const account = await db.findObject(CollectionId.Users, { email }); + + if (!account) return null; + return format.from(account); + }, + getUserByAccount: async ( + providerAccountId: Pick, + ) => { + const db = await dbPromise; + + console.log( + "Getting user by account:", + providerAccountId.providerAccountId, + ); + + const account = await db.findObject(CollectionId.Accounts, { + providerAccountId: providerAccountId.providerAccountId, + }); + + if (!account) return null; + + const user = await db.findObjectById( + CollectionId.Users, + account.userId as any as ObjectId, + ); + + if (!user) return null; + return format.from(user); + }, + updateUser: async ( + data: Partial & Pick, + ) => { + const db = await dbPromise; + const { _id, ...user } = format.to(data); + + console.log("[AUTH] Updating user:", _id); + + const existing = await db.findObjectById( + CollectionId.Users, + new ObjectId(_id), + ); + + const result = await db.updateObjectById( + CollectionId.Users, + new ObjectId(_id), + user as Partial, + ); + + return format.from({ ...existing, ...user, _id: _id }); + }, + deleteUser: async (id: string) => { + const db = await dbPromise; + + console.log("[AUTH] Deleting user:", id); + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(id), + ); + if (!user) return null; + + const account = await db.findObject(CollectionId.Accounts, { + userId: user._id, + }); + + const session = await db.findObject(CollectionId.Sessions, { + userId: user._id, + }); + + const promises = [ + db.deleteObjectById(CollectionId.Users, new ObjectId(id)), + ]; + + if (account) { + promises.push( + db.deleteObjectById(CollectionId.Accounts, new ObjectId(account._id)), + ); + } + + if (session) { + promises.push( + db.deleteObjectById(CollectionId.Sessions, new ObjectId(session._id)), + ); + } + + await Promise.all(promises); + + return format.from(user); + }, + linkAccount: async (data: Record) => { + const db = await dbPromise; + const account = format.to(data); + + console.log( + "Linking account:", + account.providerAccountId, + "User:", + account.userId, + ); + + await db.addObject(CollectionId.Accounts, account); + + return account; + }, + unlinkAccount: async ( + providerAccountId: Pick, + ) => { + const db = await dbPromise; + + console.log("[AUTH] Unlinking account:", providerAccountId.providerAccountId); + + const account = await db.findObject(CollectionId.Accounts, { + providerAccountId: providerAccountId.providerAccountId, + }); + + if (!account) return null; + + await db.deleteObjectById( + CollectionId.Accounts, + new ObjectId(account._id), + ); + + return format.from(account); + }, + getSessionAndUser: async (sessionToken: string) => { + const db = await dbPromise; + + console.log("[AUTH] Getting session and user:", sessionToken); + + const session = await db.findObject(CollectionId.Sessions, { + sessionToken, + }); + + if (!session) return null; + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(session.userId), + ); + + if (!user) return null; + return { + session: format.from(session), + user: format.from(user), + }; + }, + createSession: async (data: Record) => { + const db = await dbPromise; + + const session = format.to(data); + + console.log("[AUTH] Creating session:", session.sessionToken); + + session.userId = new ObjectId(session.userId) as any; + + await db.addObject(CollectionId.Sessions, session as unknown as Session); + + return format.from(session); + }, + updateSession: async ( + data: Partial & Pick, + ) => { + const db = await dbPromise; + const { _id, ...session } = format.to(data); + + console.log("[AUTH] Updating session:", session.sessionToken); + + const existing = await db.findObject(CollectionId.Sessions, { + sessionToken: session.sessionToken, + }); + + if (!existing) return null; + + if (session.userId) { + session.userId = new ObjectId(session.userId) as any; + } + + await db.updateObjectById( + CollectionId.Sessions, + new ObjectId(existing._id), + session as unknown as Partial, + ); + + return format.from({ ...existing, ...data }); + }, + deleteSession: async (sessionToken: string) => { + const db = await dbPromise; + + console.log("[AUTH] Deleting session:", sessionToken); + + const session = await db.findObject(CollectionId.Sessions, { + sessionToken, + }); + + if (!session) return null; + + await db.deleteObjectById( + CollectionId.Sessions, + new ObjectId(session._id), + ); + + return format.from(session); + }, + createVerificationToken: async (token: VerificationToken) => { + const db = await dbPromise; + + console.log("[AUTH] Creating verification token:", token.identifier); + + await db.addObject( + CollectionId.VerificationTokens, + format.to(token) as VerificationToken, + ); + return token; + }, + useVerificationToken: async (token: { + identifier: string; + token: string; + }) => { + const db = await dbPromise; + + console.log("[AUTH] Using verification token:", token.identifier); + + const existing = await db.findObject(CollectionId.VerificationTokens, { + token: token.token, + }); + + if (!existing) return null; + + await db.deleteObjectById( + CollectionId.VerificationTokens, + new ObjectId(existing._id), + ); + + return format.from(existing); + }, + }; + + return adapter; +} From e4dd9d16619b5549acd975320dbf04fab86ef00b Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 18:03:05 -0500 Subject: [PATCH 27/69] Reapply "Merge branch 'main' of github.com:Decatur-Robotics/Gearbox" This reverts commit bdad8f389c63cf171c3f763840c23e20df74f95a. --- .env.test | 4 +- lib/Auth.ts | 8 +- lib/Types.ts | 3 + lib/client/CollectionId.ts | 27 ++--- package-lock.json | 4 +- package.json | 2 +- tests/lib/DbInterfaceAuthAdapter.test.ts | 120 +++++++++++++++++++++++ 7 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 tests/lib/DbInterfaceAuthAdapter.test.ts diff --git a/.env.test b/.env.test index 67bb80a9..3eae1f85 100644 --- a/.env.test +++ b/.env.test @@ -4,4 +4,6 @@ DEVELOPER_EMAILS=["test@gmail.com"] TOA_URL=https://example.com TOA_APP_ID=123 -TOA_KEY=456 \ No newline at end of file +TOA_KEY=456 + +DEFAULT_IMAGE=https://example.com/default.jpg \ No newline at end of file diff --git a/lib/Auth.ts b/lib/Auth.ts index d7df0093..f42bfb0c 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -13,10 +13,10 @@ import ResendUtils from "./ResendUtils"; import CollectionId from "./client/CollectionId"; import { AdapterUser } from "next-auth/adapters"; import { wait } from "./client/ClientUtils"; - -const adapter = MongoDBAdapter(clientPromise, { databaseName: process.env.DB }); +import MongoAuthAdapter from "./DbInterfaceAuthAdapter"; const cachedDb = getDatabase(); +const adapter = MongoAuthAdapter(cachedDb); export const AuthenticationOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, @@ -83,9 +83,7 @@ export const AuthenticationOptions: AuthOptions = { ], callbacks: { async session({ session, user }) { - session.user = await ( - await cachedDb - ).findObjectById(CollectionId.Users, new ObjectId(user.id)); + session.user = user; return session; }, diff --git a/lib/Types.ts b/lib/Types.ts index 52539d0a..d006e7f0 100644 --- a/lib/Types.ts +++ b/lib/Types.ts @@ -30,6 +30,9 @@ export interface Account extends NextAuthAccount { export interface Session extends NextAuthSession { _id: string; + sessionToken: string; + userId: ObjectId; + expires: string; } export class User implements NextAuthUser { diff --git a/lib/client/CollectionId.ts b/lib/client/CollectionId.ts index 5eb209a3..00c0c646 100644 --- a/lib/client/CollectionId.ts +++ b/lib/client/CollectionId.ts @@ -1,3 +1,4 @@ +import { VerificationToken } from "next-auth/adapters"; import { Season, Competition, @@ -12,6 +13,7 @@ import { CompPicklistGroup, WebhookHolder, } from "../Types"; +import { ObjectId } from "bson"; enum CollectionId { Seasons = "Seasons", @@ -22,6 +24,7 @@ enum CollectionId { Users = "users", Accounts = "accounts", Sessions = "sessions", + VerificationTokens = "verification_tokens", Forms = "Forms", PitReports = "Pitreports", Picklists = "Picklists", @@ -51,14 +54,16 @@ export type CollectionIdToType = ? Account : Id extends CollectionId.Sessions ? Session - : Id extends CollectionId.PitReports - ? Pitreport - : Id extends CollectionId.Picklists - ? CompPicklistGroup - : Id extends CollectionId.SubjectiveReports - ? SubjectiveReport - : Id extends CollectionId.Webhooks - ? WebhookHolder - : Id extends CollectionId.Misc - ? any - : any; + : Id extends CollectionId.VerificationTokens + ? VerificationToken & { _id: ObjectId } + : Id extends CollectionId.PitReports + ? Pitreport + : Id extends CollectionId.Picklists + ? CompPicklistGroup + : Id extends CollectionId.SubjectiveReports + ? SubjectiveReport + : Id extends CollectionId.Webhooks + ? WebhookHolder + : Id extends CollectionId.Misc + ? any + : any; diff --git a/package-lock.json b/package-lock.json index 677158b3..346c0995 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sj3", - "version": "1.2.3", + "version": "1.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.2.3", + "version": "1.2.5", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", diff --git a/package.json b/package.json index 558cd6bb..d6f4a5de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.3", + "version": "1.2.5", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", diff --git a/tests/lib/DbInterfaceAuthAdapter.test.ts b/tests/lib/DbInterfaceAuthAdapter.test.ts new file mode 100644 index 00000000..a76bef4d --- /dev/null +++ b/tests/lib/DbInterfaceAuthAdapter.test.ts @@ -0,0 +1,120 @@ +import CollectionId from "@/lib/client/CollectionId"; +import InMemoryDbInterface from "@/lib/client/dbinterfaces/InMemoryDbInterface"; +import DbInterfaceAuthAdapter from "@/lib/DbInterfaceAuthAdapter"; +import { _id } from "@next-auth/mongodb-adapter"; +import { ObjectId } from "bson"; +import { get } from "http"; + +const prototype = DbInterfaceAuthAdapter(undefined as any); + +async function getDatabase() {} + +async function getAdapterAndDb() { + const db = new InMemoryDbInterface(); + await db.init(); + + return { + adapter: DbInterfaceAuthAdapter(Promise.resolve(db)), + db, + }; +} + +describe(prototype.createUser.name, () => { + test("Adds a user to the database", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + name: "Test User", + email: "test@gmail.com", + image: "test.png", + }; + + await adapter.createUser(user); + + const foundUser = await db.findObject(CollectionId.Users, { + email: user.email, + }); + + expect(foundUser).toMatchObject(user); + }); + + test("Populates fields with default values", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + name: "Test User", + email: "test@gmail.com", + image: "test.png", + }; + + await adapter.createUser(user); + + const foundUser = await db.findObject(CollectionId.Users, { + email: user.email, + }); + + expect(foundUser?.name).toBeDefined(); + expect(foundUser?.email).toBeDefined(); + expect(foundUser?.image).toBeDefined; + expect(foundUser?.admin).toBeDefined(); + expect(foundUser?.slug).toBeDefined(); + expect(foundUser?.teams).toBeDefined(); + expect(foundUser?.owner).toBeDefined(); + expect(foundUser?.level).toBeDefined(); + expect(foundUser?.xp).toBeDefined(); + }); + + test("Populates missing fields with defaults", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + email: "test@gmail.com", + }; + + await adapter.createUser(user); + + const foundUser = await db.findObject(CollectionId.Users, { + email: user.email, + }); + + expect(foundUser?.name).toBeDefined(); + expect(foundUser?.image).toBeDefined(); + }); +}); + +describe(prototype.getUser!.name, () => { + test("Returns a user from the database without their _id", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + _id: new ObjectId(), + name: "Test User", + email: "test@gmail.com", + image: "test.png", + }; + + await db.addObject(CollectionId.Users, user as any); + + const foundUser = await adapter.getUser!(user._id.toString()); + + const { _id, ...userWithoutId } = user; + + expect(foundUser).toMatchObject(userWithoutId); + }); + + test("Returns null if given an id of the wrong length", async () => { + const { adapter } = await getAdapterAndDb(); + + const foundUser = await adapter.getUser!("1234567890123456789012345"); + + expect(foundUser).toBeNull(); + }); + + test("Returns null if the user doesn't exist", async () => { + const { adapter } = await getAdapterAndDb(); + + const foundUser = await adapter.getUser!(new ObjectId().toString()); + + expect(foundUser).toBeNull(); + }); +}); From 41c1d7dfd72bbf9fd5a7e0da835c0cc835a95ca7 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 18:04:00 -0500 Subject: [PATCH 28/69] Use old adapter --- lib/Auth.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index f42bfb0c..e5e3d911 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -16,7 +16,10 @@ import { wait } from "./client/ClientUtils"; import MongoAuthAdapter from "./DbInterfaceAuthAdapter"; const cachedDb = getDatabase(); -const adapter = MongoAuthAdapter(cachedDb); +// const adapter = MongoAuthAdapter(cachedDb); +const adapter = MongoDBAdapter(clientPromise, { + databaseName: process.env.DB, +}); export const AuthenticationOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, From 59ee9f0c004a49b7f2534b5b522dd371da4a6341 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Tue, 25 Feb 2025 22:02:39 -0500 Subject: [PATCH 29/69] Fix sessions and OAuth --- lib/Auth.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index e5e3d911..01b20623 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -27,6 +27,7 @@ export const AuthenticationOptions: AuthOptions = { Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET, + allowDangerousEmailAccountLinking: true, profile: async (profile) => { const user = new User( profile.name, @@ -55,6 +56,7 @@ export const AuthenticationOptions: AuthOptions = { SlackProvider({ clientId: process.env.NEXT_PUBLIC_SLACK_CLIENT_ID as string, clientSecret: process.env.SLACK_CLIENT_SECRET as string, + allowDangerousEmailAccountLinking: true, profile: async (profile) => { const user = new User( profile.name, @@ -86,7 +88,9 @@ export const AuthenticationOptions: AuthOptions = { ], callbacks: { async session({ session, user }) { - session.user = user; + session.user = await ( + await cachedDb + ).findObjectById(CollectionId.Users, new ObjectId(user.id)); return session; }, From 53906462f93c8c32f04087a9b2c1ebede5f35f44 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Wed, 26 Feb 2025 20:45:15 -0500 Subject: [PATCH 30/69] More logs for sign in --- lib/Auth.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/Auth.ts b/lib/Auth.ts index 01b20623..125c1ae5 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -155,6 +155,12 @@ export const AuthenticationOptions: AuthOptions = { new ResendUtils().createContact(typedUser as User); + console.log( + "User is signed in:", + typedUser.name, + typedUser.email, + typedUser._id?.toString(), + ); return true; }, }, From b0b26ebd01eb41defcbabf4314e20bda470e3d81 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Wed, 26 Feb 2025 20:51:44 -0500 Subject: [PATCH 31/69] Add timer for sign in flow --- lib/Auth.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 125c1ae5..c5c17c76 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -13,10 +13,10 @@ import ResendUtils from "./ResendUtils"; import CollectionId from "./client/CollectionId"; import { AdapterUser } from "next-auth/adapters"; import { wait } from "./client/ClientUtils"; -import MongoAuthAdapter from "./DbInterfaceAuthAdapter"; +import DbInterfaceAuthAdapter from "./DbInterfaceAuthAdapter"; const cachedDb = getDatabase(); -// const adapter = MongoAuthAdapter(cachedDb); +// const adapter = DbInterfaceAuthAdapter(cachedDb); const adapter = MongoDBAdapter(clientPromise, { databaseName: process.env.DB, }); @@ -103,6 +103,7 @@ export const AuthenticationOptions: AuthOptions = { * For email sign in, runs when the "Sign In" button is clicked (before email is sent). */ async signIn({ user }) { + const startTime = Date.now(); console.log( `User is signing in: ${user.name}, ${user.email}, ${user.id}`, ); @@ -155,11 +156,16 @@ export const AuthenticationOptions: AuthOptions = { new ResendUtils().createContact(typedUser as User); + const endTime = Date.now(); + const elapsedTime = endTime - startTime; + console.log( "User is signed in:", typedUser.name, typedUser.email, typedUser._id?.toString(), + "Elapsed time:", + elapsedTime + "ms", ); return true; }, From afcc356479a5622aae1eddddcbf7156dd2afc5bd Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Wed, 26 Feb 2025 21:09:19 -0500 Subject: [PATCH 32/69] Switch back to new DB adapter --- lib/Auth.ts | 12 +++++------ lib/DbInterfaceAuthAdapter.ts | 5 ++++- lib/Utils.ts | 38 ++++++++++++++++++++++------------- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index c5c17c76..4f706b06 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -16,10 +16,10 @@ import { wait } from "./client/ClientUtils"; import DbInterfaceAuthAdapter from "./DbInterfaceAuthAdapter"; const cachedDb = getDatabase(); -// const adapter = DbInterfaceAuthAdapter(cachedDb); -const adapter = MongoDBAdapter(clientPromise, { - databaseName: process.env.DB, -}); +const adapter = DbInterfaceAuthAdapter(cachedDb); +// const adapter = MongoDBAdapter(clientPromise, { +// databaseName: process.env.DB, +// }); export const AuthenticationOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, @@ -88,9 +88,7 @@ export const AuthenticationOptions: AuthOptions = { ], callbacks: { async session({ session, user }) { - session.user = await ( - await cachedDb - ).findObjectById(CollectionId.Users, new ObjectId(user.id)); + session.user = user; return session; }, diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index fe11ce36..89ead64e 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -174,7 +174,10 @@ export default function DbInterfaceAuthAdapter( ) => { const db = await dbPromise; - console.log("[AUTH] Unlinking account:", providerAccountId.providerAccountId); + console.log( + "[AUTH] Unlinking account:", + providerAccountId.providerAccountId, + ); const account = await db.findObject(CollectionId.Accounts, { providerAccountId: providerAccountId.providerAccountId, diff --git a/lib/Utils.ts b/lib/Utils.ts index af274cff..ad64a0b8 100644 --- a/lib/Utils.ts +++ b/lib/Utils.ts @@ -92,6 +92,29 @@ export function mentionUserInSlack(user: { return user.slackId ? `<@${user.slackId}>` : (user.name ?? ""); } +export function populateMissingUserFields(user: Partial): User { + const filled: Omit = { + id: user.id ?? "", + name: user.name ?? "", + image: user.image ?? "https://4026.org/user.jpg", + slug: user.slug ?? "", + email: user.email ?? "", + teams: user.teams ?? [], + owner: user.owner ?? [], + slackId: user.slackId ?? "", + onboardingComplete: user.onboardingComplete ?? false, + admin: user.admin ?? false, + xp: user.xp ?? 0, + level: user.level ?? 0, + resendContactId: user.resendContactId ?? undefined, + lastSignInDateTime: user.lastSignInDateTime ?? undefined, + }; + + if (user._id) (filled as User)._id = user._id as unknown as string; + + return filled as User; +} + /** * If a user is missing fields, this function will populate them with default values and update the user in the DB. * @@ -125,20 +148,7 @@ export async function repairUser( const name = user.name ?? user.email?.split("@")[0] ?? "Unknown User"; // User is incomplete, fill in the missing fields - user = { - ...user, - id: id?.toString(), - name, - image: user.image ?? "https://4026.org/user.jpg", - slug: user.slug ?? (await GenerateSlug(db, CollectionId.Users, name)), - teams: user.teams ?? [], - owner: user.owner ?? [], - slackId: user.slackId ?? "", - onboardingComplete: user.onboardingComplete ?? false, - admin: user.admin ?? false, - xp: user.xp ?? 0, - level: user.level ?? 0, - } as User; + user = populateMissingUserFields(user); if (updateDocument) { await db.updateObjectById( From fefa87f6b32f7be95a56556ec4cd5118353e71a6 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Wed, 26 Feb 2025 21:17:31 -0500 Subject: [PATCH 33/69] Fix populateMissingUserFields --- lib/Utils.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/Utils.ts b/lib/Utils.ts index ad64a0b8..a8283f09 100644 --- a/lib/Utils.ts +++ b/lib/Utils.ts @@ -92,12 +92,17 @@ export function mentionUserInSlack(user: { return user.slackId ? `<@${user.slackId}>` : (user.name ?? ""); } -export function populateMissingUserFields(user: Partial): User { +export async function populateMissingUserFields( + user: Partial, + generateSlug: (name: string) => Promise, +): Promise { + const name = user.name ?? user.email?.split("@")[0] ?? "Unknown User"; + const filled: Omit = { - id: user.id ?? "", - name: user.name ?? "", + id: user.id ?? user._id?.toString() ?? new ObjectId().toString(), + name, image: user.image ?? "https://4026.org/user.jpg", - slug: user.slug ?? "", + slug: user.slug ?? (await generateSlug(name ?? "Unknown User")), email: user.email ?? "", teams: user.teams ?? [], owner: user.owner ?? [], @@ -148,7 +153,9 @@ export async function repairUser( const name = user.name ?? user.email?.split("@")[0] ?? "Unknown User"; // User is incomplete, fill in the missing fields - user = populateMissingUserFields(user); + user = await populateMissingUserFields(user, async (name) => + GenerateSlug(db, CollectionId.Users, name), + ); if (updateDocument) { await db.updateObjectById( From 0506ae87a0e6976f0331330e3bae1a858573bd5e Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Wed, 26 Feb 2025 21:34:16 -0500 Subject: [PATCH 34/69] Don't repair users in email sign in anymore --- lib/Auth.ts | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 4f706b06..38afa6ab 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -110,32 +110,32 @@ export const AuthenticationOptions: AuthOptions = { const db = await getDatabase(false); let typedUser = user as Partial; - if (!typedUser.slug || typedUser._id?.toString() != typedUser.id) { - const repairUserOnceItIsInDb = async () => { - console.log( - "User is incomplete, waiting for it to be in the database.", - ); - let foundUser: User | undefined = undefined; - while (!foundUser) { - foundUser = await db.findObject(CollectionId.Users, { - email: typedUser.email, - }); + // if (!typedUser.slug || typedUser._id?.toString() != typedUser.id) { + // const repairUserOnceItIsInDb = async () => { + // console.log( + // "User is incomplete, waiting for it to be in the database.", + // ); + // let foundUser: User | undefined = undefined; + // while (!foundUser) { + // foundUser = await db.findObject(CollectionId.Users, { + // email: typedUser.email, + // }); - if (!foundUser) await wait(50); - } + // if (!foundUser) await wait(50); + // } - console.log("User is incomplete, filling in missing fields."); + // console.log("User is incomplete, filling in missing fields."); - typedUser._id = foundUser._id; - typedUser.lastSignInDateTime = new Date(); + // typedUser._id = foundUser._id; + // typedUser.lastSignInDateTime = new Date(); - typedUser = await repairUser(db, typedUser); + // typedUser = await repairUser(db, typedUser); - console.log("User updated:", typedUser._id?.toString()); - }; + // console.log("User updated:", typedUser._id?.toString()); + // }; - repairUserOnceItIsInDb(); - } + // repairUserOnceItIsInDb(); + // } const today = new Date(); if ( From 7eb2518d700b8f2166bacd1b8c9046a0b663a2ff Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Wed, 26 Feb 2025 21:45:10 -0500 Subject: [PATCH 35/69] Don't overwrite existing users --- lib/DbInterfaceAuthAdapter.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index 89ead64e..3a6c30fe 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -23,6 +23,17 @@ export default function DbInterfaceAuthAdapter( console.log("[AUTH] Creating user:", adapterUser.name); + // Check if user already exists + const existingUser = await db.findObject(CollectionId.Users, { + email: adapterUser.email, + }); + + if (existingUser) { + // If user exists, return existing user + console.log("[AUTH] User already exists:", existingUser.name); + return format.from(existingUser); + } + const user = new User( adapterUser.name ?? "Unknown", adapterUser.email, From ddd599a9c6c1a8a43dd31c3f73128fee50433e06 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 15:43:44 -0500 Subject: [PATCH 36/69] Add [AUTH] prefix to authentication logs for better clarity --- lib/Auth.ts | 11 +++++++++-- lib/DbInterfaceAuthAdapter.ts | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 38afa6ab..396eb69b 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -103,13 +103,20 @@ export const AuthenticationOptions: AuthOptions = { async signIn({ user }) { const startTime = Date.now(); console.log( - `User is signing in: ${user.name}, ${user.email}, ${user.id}`, + `[AUTH] User is signing in: ${user.name}, ${user.email}, ${user.id}`, ); Analytics.signIn(user.name ?? "Unknown User"); const db = await getDatabase(false); let typedUser = user as Partial; + + const existingUser = await db.findObject(CollectionId.Users, { + email: typedUser.email, + }); + + typedUser._id = existingUser?._id; + // if (!typedUser.slug || typedUser._id?.toString() != typedUser.id) { // const repairUserOnceItIsInDb = async () => { // console.log( @@ -158,7 +165,7 @@ export const AuthenticationOptions: AuthOptions = { const elapsedTime = endTime - startTime; console.log( - "User is signed in:", + "[AUTH] User is signed in:", typedUser.name, typedUser.email, typedUser._id?.toString(), diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index 3a6c30fe..b389d785 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -87,7 +87,7 @@ export default function DbInterfaceAuthAdapter( const db = await dbPromise; console.log( - "Getting user by account:", + "[AUTH] Getting user by account:", providerAccountId.providerAccountId, ); @@ -170,7 +170,7 @@ export default function DbInterfaceAuthAdapter( const account = format.to(data); console.log( - "Linking account:", + "[AUTH] Linking account:", account.providerAccountId, "User:", account.userId, From 1dff377f426eeb7fcb48a60619b40edaaa95fc38 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 15:47:30 -0500 Subject: [PATCH 37/69] Use cache in signIn DB --- lib/Auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 396eb69b..057083a7 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -107,7 +107,7 @@ export const AuthenticationOptions: AuthOptions = { ); Analytics.signIn(user.name ?? "Unknown User"); - const db = await getDatabase(false); + const db = await getDatabase(); let typedUser = user as Partial; From cfa6e7dc6263a4b165f8d5f8de2876045c001296 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 15:59:59 -0500 Subject: [PATCH 38/69] Better logs for creating a session --- lib/DbInterfaceAuthAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index b389d785..e65b50d0 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -230,7 +230,7 @@ export default function DbInterfaceAuthAdapter( const session = format.to(data); - console.log("[AUTH] Creating session:", session.sessionToken); + console.log("[AUTH] Creating session:", session); session.userId = new ObjectId(session.userId) as any; From 725d36b4d681df4a509565f85285ec62aa4d01ee Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 16:12:38 -0500 Subject: [PATCH 39/69] Don't create duplicate accounts --- lib/DbInterfaceAuthAdapter.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index e65b50d0..c7ba2dc8 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -176,6 +176,18 @@ export default function DbInterfaceAuthAdapter( account.userId, ); + const existing = await db.findObject(CollectionId.Accounts, { + providerAccountId: account.providerAccountId, + }); + + if (existing) { + console.log( + "[AUTH] Account already exists:", + existing.providerAccountId, + ); + return format.from(existing); + } + await db.addObject(CollectionId.Accounts, account); return account; From e437dc7f66b7f80436a6ff76916e555147d225aa Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 16:29:02 -0500 Subject: [PATCH 40/69] More logs --- lib/DbInterfaceAuthAdapter.ts | 85 ++++++++++++++++-------- lib/client/Logger.ts | 57 ++++++++++++++++ tests/lib/DbInterfaceAuthAdapter.test.ts | 2 +- 3 files changed, 115 insertions(+), 29 deletions(-) create mode 100644 lib/client/Logger.ts diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index c7ba2dc8..46dfef96 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -11,17 +11,21 @@ import CollectionId from "./client/CollectionId"; import { User, Session } from "./Types"; import { GenerateSlug } from "./Utils"; import { ObjectId } from "bson"; +import Logger from "./client/Logger"; export default function DbInterfaceAuthAdapter( dbPromise: Promise, + enableLogs: boolean = true, ): Adapter { + const logger = new Logger(["AUTH"], enableLogs); + const adapter: Adapter = { createUser: async (data: Record) => { const db = await dbPromise; const adapterUser = format.to(data); - console.log("[AUTH] Creating user:", adapterUser.name); + logger.debug("Creating user:", adapterUser.name); // Check if user already exists const existingUser = await db.findObject(CollectionId.Users, { @@ -30,7 +34,7 @@ export default function DbInterfaceAuthAdapter( if (existingUser) { // If user exists, return existing user - console.log("[AUTH] User already exists:", existingUser.name); + logger.warn("User already exists:", existingUser.name); return format.from(existingUser); } @@ -61,7 +65,7 @@ export default function DbInterfaceAuthAdapter( if (id.length !== 24) return null; - console.log("[AUTH] Getting user:", id); + logger.debug("Getting user:", id); const user = await db.findObjectById( CollectionId.Users, @@ -74,7 +78,7 @@ export default function DbInterfaceAuthAdapter( getUserByEmail: async (email: string) => { const db = await dbPromise; - console.log("[AUTH] Getting user by email:", email); + logger.debug("Getting user by email:", email); const account = await db.findObject(CollectionId.Users, { email }); @@ -86,8 +90,8 @@ export default function DbInterfaceAuthAdapter( ) => { const db = await dbPromise; - console.log( - "[AUTH] Getting user by account:", + logger.debug( + "Getting user by account:", providerAccountId.providerAccountId, ); @@ -95,7 +99,10 @@ export default function DbInterfaceAuthAdapter( providerAccountId: providerAccountId.providerAccountId, }); - if (!account) return null; + if (!account) { + logger.warn("Account not found:", providerAccountId.provider); + return null; + } const user = await db.findObjectById( CollectionId.Users, @@ -111,14 +118,14 @@ export default function DbInterfaceAuthAdapter( const db = await dbPromise; const { _id, ...user } = format.to(data); - console.log("[AUTH] Updating user:", _id); + logger.debug("Updating user:", _id); const existing = await db.findObjectById( CollectionId.Users, new ObjectId(_id), ); - const result = await db.updateObjectById( + await db.updateObjectById( CollectionId.Users, new ObjectId(_id), user as Partial, @@ -129,7 +136,7 @@ export default function DbInterfaceAuthAdapter( deleteUser: async (id: string) => { const db = await dbPromise; - console.log("[AUTH] Deleting user:", id); + logger.log("Deleting user:", id); const user = await db.findObjectById( CollectionId.Users, @@ -169,8 +176,8 @@ export default function DbInterfaceAuthAdapter( const db = await dbPromise; const account = format.to(data); - console.log( - "[AUTH] Linking account:", + logger.debug( + "Linking account:", account.providerAccountId, "User:", account.userId, @@ -181,8 +188,8 @@ export default function DbInterfaceAuthAdapter( }); if (existing) { - console.log( - "[AUTH] Account already exists:", + logger.warn( + "Account already exists:", existing.providerAccountId, ); return format.from(existing); @@ -197,8 +204,8 @@ export default function DbInterfaceAuthAdapter( ) => { const db = await dbPromise; - console.log( - "[AUTH] Unlinking account:", + logger.debug( + "Unlinking account:", providerAccountId.providerAccountId, ); @@ -206,7 +213,13 @@ export default function DbInterfaceAuthAdapter( providerAccountId: providerAccountId.providerAccountId, }); - if (!account) return null; + if (!account) { + logger.warn( + "Account not found:", + providerAccountId.providerAccountId, + ); + return null; + } await db.deleteObjectById( CollectionId.Accounts, @@ -218,20 +231,27 @@ export default function DbInterfaceAuthAdapter( getSessionAndUser: async (sessionToken: string) => { const db = await dbPromise; - console.log("[AUTH] Getting session and user:", sessionToken); + logger.debug("Getting session and user:", sessionToken); const session = await db.findObject(CollectionId.Sessions, { sessionToken, }); - if (!session) return null; + if (!session) { + logger.warn("Session not found:", sessionToken); + return null; + } const user = await db.findObjectById( CollectionId.Users, new ObjectId(session.userId), ); - if (!user) return null; + if (!user) { + logger.warn("User not found:", session.userId); + return null; + } + return { session: format.from(session), user: format.from(user), @@ -242,7 +262,7 @@ export default function DbInterfaceAuthAdapter( const session = format.to(data); - console.log("[AUTH] Creating session:", session); + logger.debug("Creating session:", session); session.userId = new ObjectId(session.userId) as any; @@ -256,13 +276,16 @@ export default function DbInterfaceAuthAdapter( const db = await dbPromise; const { _id, ...session } = format.to(data); - console.log("[AUTH] Updating session:", session.sessionToken); + logger.debug("Updating session:", session.sessionToken); const existing = await db.findObject(CollectionId.Sessions, { sessionToken: session.sessionToken, }); - if (!existing) return null; + if (!existing) { + logger.warn("Session not found:", session.sessionToken); + return null; + } if (session.userId) { session.userId = new ObjectId(session.userId) as any; @@ -279,13 +302,16 @@ export default function DbInterfaceAuthAdapter( deleteSession: async (sessionToken: string) => { const db = await dbPromise; - console.log("[AUTH] Deleting session:", sessionToken); + logger.debug("Deleting session:", sessionToken); const session = await db.findObject(CollectionId.Sessions, { sessionToken, }); - if (!session) return null; + if (!session) { + logger.warn("Session not found:", sessionToken); + return null; + } await db.deleteObjectById( CollectionId.Sessions, @@ -297,7 +323,7 @@ export default function DbInterfaceAuthAdapter( createVerificationToken: async (token: VerificationToken) => { const db = await dbPromise; - console.log("[AUTH] Creating verification token:", token.identifier); + logger.debug("Creating verification token:", token.identifier); await db.addObject( CollectionId.VerificationTokens, @@ -311,13 +337,16 @@ export default function DbInterfaceAuthAdapter( }) => { const db = await dbPromise; - console.log("[AUTH] Using verification token:", token.identifier); + logger.info("Using verification token:", token.identifier); const existing = await db.findObject(CollectionId.VerificationTokens, { token: token.token, }); - if (!existing) return null; + if (!existing) { + logger.warn("Verification token not found:", token.token); + return null; + } await db.deleteObjectById( CollectionId.VerificationTokens, diff --git a/lib/client/Logger.ts b/lib/client/Logger.ts new file mode 100644 index 00000000..121cf32e --- /dev/null +++ b/lib/client/Logger.ts @@ -0,0 +1,57 @@ +export enum LogLevel { + Error, + Warning, + Info, + Debug, +} + +export default class Logger { + constructor( + private tags: string[], + private enabled: boolean = true, + ) {} + + private prefix(level: LogLevel) { + return `[${this.tags.join(", ")}] [${LogLevel[level]}]`; + } + + public extend(tags: string[]) { + return new Logger([...this.tags, ...tags], this.enabled); + } + + public print(level: LogLevel, ...args: unknown[]) { + if (!this.enabled) return; + + const prefix = this.prefix(level); + + if (level === LogLevel.Error) { + console.error(prefix, ...args); + } else if (level === LogLevel.Warning) { + console.warn(prefix, ...args); + } else if (level === LogLevel.Info) { + console.info(prefix, ...args); + } else if (level === LogLevel.Debug) { + console.debug(prefix, ...args); + } + } + + public error(...args: unknown[]) { + this.print(LogLevel.Error, ...args); + } + + public warn(...args: unknown[]) { + this.print(LogLevel.Warning, ...args); + } + + public info(...args: unknown[]) { + this.print(LogLevel.Info, ...args); + } + + public debug(...args: unknown[]) { + this.print(LogLevel.Debug, ...args); + } + + public log(...args: unknown[]) { + this.info(...args); + } +} diff --git a/tests/lib/DbInterfaceAuthAdapter.test.ts b/tests/lib/DbInterfaceAuthAdapter.test.ts index a76bef4d..9e71862d 100644 --- a/tests/lib/DbInterfaceAuthAdapter.test.ts +++ b/tests/lib/DbInterfaceAuthAdapter.test.ts @@ -5,7 +5,7 @@ import { _id } from "@next-auth/mongodb-adapter"; import { ObjectId } from "bson"; import { get } from "http"; -const prototype = DbInterfaceAuthAdapter(undefined as any); +const prototype = DbInterfaceAuthAdapter(undefined as any, false); async function getDatabase() {} From 8eaa79e64787a34725e44981ef46887d5cac83e8 Mon Sep 17 00:00:00 2001 From: Gearbox Bot Date: Thu, 27 Feb 2025 21:29:58 +0000 Subject: [PATCH 41/69] 1.2.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 677158b3..daa00cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sj3", - "version": "1.2.3", + "version": "1.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.2.3", + "version": "1.2.4", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", diff --git a/package.json b/package.json index 558cd6bb..308a4c55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.3", + "version": "1.2.4", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", From 12d2058eeeb7ece083cde626c52f3f292acb2ff8 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 16:32:37 -0500 Subject: [PATCH 42/69] Better logs in Auth.ts --- lib/Auth.ts | 61 +++++++---------------------------- lib/DbInterfaceAuthAdapter.ts | 20 ++++-------- 2 files changed, 18 insertions(+), 63 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 057083a7..80ba48d7 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -1,25 +1,22 @@ import NextAuth, { AuthOptions } from "next-auth"; import Google from "next-auth/providers/google"; -import GitHubProvider from "next-auth/providers/github"; import SlackProvider from "next-auth/providers/slack"; -import { MongoDBAdapter } from "@next-auth/mongodb-adapter"; -import { getDatabase, clientPromise } from "./MongoDB"; +import { getDatabase } from "./MongoDB"; import { ObjectId } from "bson"; import { User } from "./Types"; -import { GenerateSlug, repairUser } from "./Utils"; +import { GenerateSlug } from "./Utils"; import { Analytics } from "@/lib/client/Analytics"; import Email from "next-auth/providers/email"; import ResendUtils from "./ResendUtils"; import CollectionId from "./client/CollectionId"; import { AdapterUser } from "next-auth/adapters"; -import { wait } from "./client/ClientUtils"; import DbInterfaceAuthAdapter from "./DbInterfaceAuthAdapter"; +import Logger from "./client/Logger"; + +const logger = new Logger(["AUTH"], true); const cachedDb = getDatabase(); const adapter = DbInterfaceAuthAdapter(cachedDb); -// const adapter = MongoDBAdapter(clientPromise, { -// databaseName: process.env.DB, -// }); export const AuthenticationOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, @@ -29,6 +26,8 @@ export const AuthenticationOptions: AuthOptions = { clientSecret: process.env.GOOGLE_SECRET, allowDangerousEmailAccountLinking: true, profile: async (profile) => { + logger.debug("Google profile:", profile); + const user = new User( profile.name, profile.email, @@ -42,22 +41,13 @@ export const AuthenticationOptions: AuthOptions = { return user; }, }), - /* - GitHubProvider({ - clientId: process.env.GITHUB_ID as string, - clientSecret: process.env.GITHUB_SECRET as string, - profile: async (profile) => { - const user = new User(profile.login, profile.email, profile.avatar_url, false, await GenerateSlug(CollectionId.Users, profile.login), [], []); - user.id = profile.id; - return user; - }, - }), - */ SlackProvider({ clientId: process.env.NEXT_PUBLIC_SLACK_CLIENT_ID as string, clientSecret: process.env.SLACK_CLIENT_SECRET as string, allowDangerousEmailAccountLinking: true, profile: async (profile) => { + logger.debug("Slack profile:", profile); + const user = new User( profile.name, profile.email, @@ -102,8 +92,8 @@ export const AuthenticationOptions: AuthOptions = { */ async signIn({ user }) { const startTime = Date.now(); - console.log( - `[AUTH] User is signing in: ${user.name}, ${user.email}, ${user.id}`, + logger.debug( + `User is signing in: ${user.name}, ${user.email}, ${user.id}`, ); Analytics.signIn(user.name ?? "Unknown User"); @@ -117,33 +107,6 @@ export const AuthenticationOptions: AuthOptions = { typedUser._id = existingUser?._id; - // if (!typedUser.slug || typedUser._id?.toString() != typedUser.id) { - // const repairUserOnceItIsInDb = async () => { - // console.log( - // "User is incomplete, waiting for it to be in the database.", - // ); - // let foundUser: User | undefined = undefined; - // while (!foundUser) { - // foundUser = await db.findObject(CollectionId.Users, { - // email: typedUser.email, - // }); - - // if (!foundUser) await wait(50); - // } - - // console.log("User is incomplete, filling in missing fields."); - - // typedUser._id = foundUser._id; - // typedUser.lastSignInDateTime = new Date(); - - // typedUser = await repairUser(db, typedUser); - - // console.log("User updated:", typedUser._id?.toString()); - // }; - - // repairUserOnceItIsInDb(); - // } - const today = new Date(); if ( (typedUser as User).lastSignInDateTime?.toDateString() !== @@ -164,7 +127,7 @@ export const AuthenticationOptions: AuthOptions = { const endTime = Date.now(); const elapsedTime = endTime - startTime; - console.log( + logger.log( "[AUTH] User is signed in:", typedUser.name, typedUser.email, diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index 46dfef96..ff41e6a7 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -15,9 +15,10 @@ import Logger from "./client/Logger"; export default function DbInterfaceAuthAdapter( dbPromise: Promise, - enableLogs: boolean = true, + baseLogger?: Logger, ): Adapter { - const logger = new Logger(["AUTH"], enableLogs); + const logger = + (baseLogger && baseLogger.extend(["ADAPTER"])) ?? new Logger(["AUTH"], false); const adapter: Adapter = { createUser: async (data: Record) => { @@ -188,10 +189,7 @@ export default function DbInterfaceAuthAdapter( }); if (existing) { - logger.warn( - "Account already exists:", - existing.providerAccountId, - ); + logger.warn("Account already exists:", existing.providerAccountId); return format.from(existing); } @@ -204,20 +202,14 @@ export default function DbInterfaceAuthAdapter( ) => { const db = await dbPromise; - logger.debug( - "Unlinking account:", - providerAccountId.providerAccountId, - ); + logger.debug("Unlinking account:", providerAccountId.providerAccountId); const account = await db.findObject(CollectionId.Accounts, { providerAccountId: providerAccountId.providerAccountId, }); if (!account) { - logger.warn( - "Account not found:", - providerAccountId.providerAccountId, - ); + logger.warn("Account not found:", providerAccountId.providerAccountId); return null; } From e10f2bf39e8934f8e335e40148ee1950ed77b089 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 16:35:16 -0500 Subject: [PATCH 43/69] Fix tests --- lib/DbInterfaceAuthAdapter.ts | 16 +++++++++++++--- tests/lib/DbInterfaceAuthAdapter.test.ts | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index ff41e6a7..45dfc041 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -13,12 +13,16 @@ import { GenerateSlug } from "./Utils"; import { ObjectId } from "bson"; import Logger from "./client/Logger"; +/** + * @tested_by tests/lib/DbInterfaceAuthAdapter.test.ts + */ export default function DbInterfaceAuthAdapter( dbPromise: Promise, baseLogger?: Logger, ): Adapter { const logger = - (baseLogger && baseLogger.extend(["ADAPTER"])) ?? new Logger(["AUTH"], false); + (baseLogger && baseLogger.extend(["ADAPTER"])) ?? + new Logger(["AUTH"], false); const adapter: Adapter = { createUser: async (data: Record) => { @@ -110,7 +114,10 @@ export default function DbInterfaceAuthAdapter( account.userId as any as ObjectId, ); - if (!user) return null; + if (!user) { + logger.warn("User not found:", account.userId); + return null; + } return format.from(user); }, updateUser: async ( @@ -143,7 +150,10 @@ export default function DbInterfaceAuthAdapter( CollectionId.Users, new ObjectId(id), ); - if (!user) return null; + if (!user) { + logger.warn("User not found:", id); + return null; + } const account = await db.findObject(CollectionId.Accounts, { userId: user._id, diff --git a/tests/lib/DbInterfaceAuthAdapter.test.ts b/tests/lib/DbInterfaceAuthAdapter.test.ts index 9e71862d..a76bef4d 100644 --- a/tests/lib/DbInterfaceAuthAdapter.test.ts +++ b/tests/lib/DbInterfaceAuthAdapter.test.ts @@ -5,7 +5,7 @@ import { _id } from "@next-auth/mongodb-adapter"; import { ObjectId } from "bson"; import { get } from "http"; -const prototype = DbInterfaceAuthAdapter(undefined as any, false); +const prototype = DbInterfaceAuthAdapter(undefined as any); async function getDatabase() {} From f96b0e11d7d7834ce1d58487cc32ba7db2e3151c Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 16:48:28 -0500 Subject: [PATCH 44/69] Return DB session --- lib/Auth.ts | 2 +- lib/DbInterfaceAuthAdapter.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 80ba48d7..085d310a 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -128,7 +128,7 @@ export const AuthenticationOptions: AuthOptions = { const elapsedTime = endTime - startTime; logger.log( - "[AUTH] User is signed in:", + "User is signed in:", typedUser.name, typedUser.email, typedUser._id?.toString(), diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index 45dfc041..0d7e439c 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -260,17 +260,19 @@ export default function DbInterfaceAuthAdapter( }; }, createSession: async (data: Record) => { - const db = await dbPromise; + logger.debug("Creating session:", data); + const db = await dbPromise; const session = format.to(data); - logger.debug("Creating session:", session); - session.userId = new ObjectId(session.userId) as any; - await db.addObject(CollectionId.Sessions, session as unknown as Session); + const dbSession = await db.addObject( + CollectionId.Sessions, + session as unknown as Session, + ); - return format.from(session); + return format.from(dbSession); }, updateSession: async ( data: Partial & Pick, From ccdac59b0c3594d09936cddea4e354dcb32a2142 Mon Sep 17 00:00:00 2001 From: Teknosquad5219 Date: Thu, 27 Feb 2025 17:04:41 -0500 Subject: [PATCH 45/69] Removed two unintentional indents --- package-lock.json | 1 - package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3b49556c..04f606b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,5 @@ { "name": "sj3", - "version": "1.2.5", "lockfileVersion": 3, "requires": true, diff --git a/package.json b/package.json index 7c2bcab2..d6f4a5de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "sj3", - "version": "1.2.5", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", From 3ab179c9a1be6e9b7cb43af484be52b1f61a64ec Mon Sep 17 00:00:00 2001 From: Teknosquad5219 Date: Thu, 27 Feb 2025 17:05:56 -0500 Subject: [PATCH 46/69] Removed yet another indent. --- package-lock.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 04f606b4..346c0995 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,6 @@ "packages": { "": { "name": "sj3", - "version": "1.2.5", "license": "CC BY-NC-SA 4.0", "dependencies": { From 1d49bb9a6ed264668fd9b1ad87794bfc51cec548 Mon Sep 17 00:00:00 2001 From: Teknosquad5219 Date: Thu, 27 Feb 2025 17:11:09 -0500 Subject: [PATCH 47/69] Attempting to please Prettier --- components/competition/InsightsAndSettingsCard.tsx | 1 + pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/components/competition/InsightsAndSettingsCard.tsx b/components/competition/InsightsAndSettingsCard.tsx index b26ac271..828274f4 100644 --- a/components/competition/InsightsAndSettingsCard.tsx +++ b/components/competition/InsightsAndSettingsCard.tsx @@ -10,6 +10,7 @@ import { FaSync, FaBinoculars, FaUserCheck, FaDatabase } from "react-icons/fa"; import { FaUserGroup } from "react-icons/fa6"; import ClientApi from "@/lib/api/ClientApi"; + const api = new ClientApi(); export default function InsightsAndSettingsCard(props: { diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx index 0309ac93..367aedb1 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx @@ -1,5 +1,6 @@ import { ChangeEvent, useEffect, useState, useCallback } from "react"; + import ClientApi from "@/lib/api/ClientApi"; import { Match, From 60175e528db89a04eebe8ad7d489c57d715a9128 Mon Sep 17 00:00:00 2001 From: Teknosquad5219 Date: Thu, 27 Feb 2025 17:14:01 -0500 Subject: [PATCH 48/69] Attempt 2 at pleasing Prettier --- .../competition/InsightsAndSettingsCard.tsx | 37 ++++++++----------- .../[seasonSlug]/[competitonSlug]/index.tsx | 3 -- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/components/competition/InsightsAndSettingsCard.tsx b/components/competition/InsightsAndSettingsCard.tsx index 828274f4..6d7d10a8 100644 --- a/components/competition/InsightsAndSettingsCard.tsx +++ b/components/competition/InsightsAndSettingsCard.tsx @@ -10,7 +10,6 @@ import { FaSync, FaBinoculars, FaUserCheck, FaDatabase } from "react-icons/fa"; import { FaUserGroup } from "react-icons/fa6"; import ClientApi from "@/lib/api/ClientApi"; - const api = new ClientApi(); export default function InsightsAndSettingsCard(props: { @@ -30,7 +29,7 @@ export default function InsightsAndSettingsCard(props: { seasonSlug: string | undefined; team: Team | undefined; }) { - const [showSettings, setShowSettings] = useState(false); + const [showSettings, setShowSettings] = useState(false); const { isManager, comp, @@ -47,15 +46,15 @@ export default function InsightsAndSettingsCard(props: { seasonSlug, team, } = props; - const [newCompName, setNewCompName] = useState(comp?.name); - const [newCompTbaId, setNewCompTbaId] = useState(comp?.tbaId); - const [exportPending, setExportPending] = useState(false); - const [teamToAdd, setTeamToAdd] = useState(0); - const [blueAlliance, setBlueAlliance] = useState([]); - const [redAlliance, setRedAlliance] = useState([]); - const [matchNumber, setMatchNumber] = useState(undefined,); + const [newCompName, setNewCompName] = useState(comp?.name); + const [newCompTbaId, setNewCompTbaId] = useState(comp?.tbaId); + const [exportPending, setExportPending] = useState(false); + const [teamToAdd, setTeamToAdd] = useState(0); + const [blueAlliance, setBlueAlliance] = useState([]); + const [redAlliance, setRedAlliance] = useState([]); + const [matchNumber, setMatchNumber] = useState(undefined); - const exportAsCsv = async () => { + const exportAsCsv = async () => { setExportPending(true); const res = await api.exportCompAsCsv(comp?._id!).catch((e) => { @@ -76,7 +75,7 @@ export default function InsightsAndSettingsCard(props: { setExportPending(false); }; - const createMatch = async () => { + const createMatch = async () => { try { await api.createMatch( comp?._id!, @@ -93,14 +92,10 @@ export default function InsightsAndSettingsCard(props: { location.reload(); }; - const allianceIndices: number[] = []; - for ( - let i = 0; - i < games[comp?.gameId ?? defaultGameId].allianceSize; - i++ - ) { - allianceIndices.push(i); - } + const allianceIndices: number[] = []; + for (let i = 0; i < games[comp?.gameId ?? defaultGameId].allianceSize; i++) { + allianceIndices.push(i); + } async function saveCompChanges() { // Check if tbaId is valid @@ -122,12 +117,12 @@ export default function InsightsAndSettingsCard(props: { location.reload(); } - function togglePublicData(e: ChangeEvent) { + function togglePublicData(e: ChangeEvent) { if (!comp?._id) return; api.setCompPublicData(comp?._id, e.target.checked); } - function addTeam() { + function addTeam() { console.log("Adding pit report for team", teamToAdd); if (!teamToAdd || teamToAdd < 1 || !comp?._id) return; diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx index 367aedb1..9a40bc17 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx @@ -1,6 +1,5 @@ import { ChangeEvent, useEffect, useState, useCallback } from "react"; - import ClientApi from "@/lib/api/ClientApi"; import { Match, @@ -50,7 +49,6 @@ export default function CompetitionIndex({ team?.owners.includes(session?.user?._id)) ?? false; - const [matches, setMatches] = useState([]); const [showSubmittedMatches, setShowSubmittedMatches] = useState(false); @@ -399,7 +397,6 @@ export default function CompetitionIndex({ api.remindSlack(team._id.toString(), userId); } - return ( Date: Thu, 27 Feb 2025 17:18:25 -0500 Subject: [PATCH 49/69] don't overwrite user IDs --- lib/DbInterfaceAuthAdapter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index 0d7e439c..ce361965 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -133,6 +133,8 @@ export default function DbInterfaceAuthAdapter( new ObjectId(_id), ); + user.id = existing?._id?.toString()!; + await db.updateObjectById( CollectionId.Users, new ObjectId(_id), From 0ff2a1b62b8a641234afb9ba10d23fe9fa143df6 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 17:27:25 -0500 Subject: [PATCH 50/69] Remove user ID assignment from authentication process and improve logging --- lib/Auth.ts | 7 +------ lib/Utils.ts | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 085d310a..35785238 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -37,7 +37,6 @@ export const AuthenticationOptions: AuthOptions = { [], [], ); - user.id = profile.sub; return user; }, }), @@ -60,7 +59,6 @@ export const AuthenticationOptions: AuthOptions = { 10, 1, ); - user.id = profile.sub; return user; }, }), @@ -92,9 +90,7 @@ export const AuthenticationOptions: AuthOptions = { */ async signIn({ user }) { const startTime = Date.now(); - logger.debug( - `User is signing in: ${user.name}, ${user.email}, ${user.id}`, - ); + logger.debug(`User is signing in: ${user.name}, ${user.email}`); Analytics.signIn(user.name ?? "Unknown User"); const db = await getDatabase(); @@ -112,7 +108,6 @@ export const AuthenticationOptions: AuthOptions = { (typedUser as User).lastSignInDateTime?.toDateString() !== today.toDateString() ) { - // We use user.id since user._id strangely doesn't exist on user. db.updateObjectById( CollectionId.Users, new ObjectId(typedUser._id?.toString()), diff --git a/lib/Utils.ts b/lib/Utils.ts index a8283f09..09503f38 100644 --- a/lib/Utils.ts +++ b/lib/Utils.ts @@ -99,7 +99,7 @@ export async function populateMissingUserFields( const name = user.name ?? user.email?.split("@")[0] ?? "Unknown User"; const filled: Omit = { - id: user.id ?? user._id?.toString() ?? new ObjectId().toString(), + id: user._id?.toString() ?? new ObjectId().toString(), name, image: user.image ?? "https://4026.org/user.jpg", slug: user.slug ?? (await generateSlug(name ?? "Unknown User")), From 12acc67b2ec82c84807c7124289ee0937b9ef819 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 17:34:03 -0500 Subject: [PATCH 51/69] Logs for AccessLevels --- lib/api/AccessLevels.ts | 14 ++++++++++++++ scripts/fixTeamMembership.ts | 8 ++++++++ scripts/loadUsersIntoResend.ts | 1 - 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 scripts/fixTeamMembership.ts diff --git a/lib/api/AccessLevels.ts b/lib/api/AccessLevels.ts index df6e5039..35e94ba0 100644 --- a/lib/api/AccessLevels.ts +++ b/lib/api/AccessLevels.ts @@ -294,6 +294,11 @@ namespace AccessLevels { pitReportId: string, ) { const user = await userPromise; + console.log( + "[AccessLevels] IfOnTeamThatOwnsPitReport", + pitReportId, + user?._id?.toString(), + ); if (!user) { return { authorized: false, authData: undefined }; } @@ -301,20 +306,29 @@ namespace AccessLevels { const pitReport = await ( await db ).findObjectById(CollectionId.PitReports, new ObjectId(pitReportId)); + console.log("[AccessLevels] pitReport", pitReport); if (!pitReport) { return { authorized: false, authData: undefined }; } const comp = await getCompFromPitReport(await db, pitReport); + console.log("[AccessLevels] comp", comp); if (!comp) { return { authorized: false, authData: undefined }; } const team = await getTeamFromComp(await db, comp); + console.log("[AccessLevels] team", team); if (!team) { return { authorized: false, authData: undefined }; } + console.log( + "[AccessLevels] team.users", + team.users, + user._id?.toString(), + team?.users.includes(user._id?.toString()!), + ); return { authorized: team?.users.includes(user._id?.toString()!), authData: { team, comp, pitReport }, diff --git a/scripts/fixTeamMembership.ts b/scripts/fixTeamMembership.ts new file mode 100644 index 00000000..ea484ace --- /dev/null +++ b/scripts/fixTeamMembership.ts @@ -0,0 +1,8 @@ +// async function fixTeamMembership() { +// console.log("Fixing team membership and ownership..."); + +// console.log("Getting database..."); +// const db = await getDatabase(); +// } + +// fixTeamMembership(); diff --git a/scripts/loadUsersIntoResend.ts b/scripts/loadUsersIntoResend.ts index 3743745b..09352172 100644 --- a/scripts/loadUsersIntoResend.ts +++ b/scripts/loadUsersIntoResend.ts @@ -1,7 +1,6 @@ import { getDatabase } from "@/lib/MongoDB"; import CollectionId from "@/lib/client/CollectionId"; import ResendUtils from "@/lib/ResendUtils"; -import { User } from "@/lib/Types"; async function loadUsersIntoResend() { console.log("Loading users into Resend..."); From 9301a0c6de36e654be3e41e70986e24dba57c5bc Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 17:48:26 -0500 Subject: [PATCH 52/69] Don't set id for existing users --- lib/Auth.ts | 21 +++++++++++++ scripts/fixTeamMembership.ts | 61 ++++++++++++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 35785238..1194c470 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -28,6 +28,13 @@ export const AuthenticationOptions: AuthOptions = { profile: async (profile) => { logger.debug("Google profile:", profile); + const existingUser = await ( + await cachedDb + ).findObject(CollectionId.Users, { email: profile.email }); + if (existingUser) { + return existingUser; + } + const user = new User( profile.name, profile.email, @@ -37,6 +44,9 @@ export const AuthenticationOptions: AuthOptions = { [], [], ); + + user.id = profile.sub; + return user; }, }), @@ -47,6 +57,14 @@ export const AuthenticationOptions: AuthOptions = { profile: async (profile) => { logger.debug("Slack profile:", profile); + const existing = await ( + await cachedDb + ).findObject(CollectionId.Users, { email: profile.email }); + + if (existing) { + return existing; + } + const user = new User( profile.name, profile.email, @@ -59,6 +77,9 @@ export const AuthenticationOptions: AuthOptions = { 10, 1, ); + + user.id = profile.sub; + return user; }, }), diff --git a/scripts/fixTeamMembership.ts b/scripts/fixTeamMembership.ts index ea484ace..9ab2fc8c 100644 --- a/scripts/fixTeamMembership.ts +++ b/scripts/fixTeamMembership.ts @@ -1,8 +1,57 @@ -// async function fixTeamMembership() { -// console.log("Fixing team membership and ownership..."); +import CollectionId from "@/lib/client/CollectionId"; +import { getDatabase } from "@/lib/MongoDB"; +import { ObjectId } from "bson"; -// console.log("Getting database..."); -// const db = await getDatabase(); -// } +async function fixTeamMembership() { + console.log("Fixing team membership and ownership..."); -// fixTeamMembership(); + console.log("Getting database..."); + const db = await getDatabase(); + + console.log("Finding teams..."); + const teams = await db.findObjects(CollectionId.Teams, {}); + + console.log(`Found ${teams.length} teams.`); + + const users: { [id: string]: { teams: string[]; owner: string[] } } = {}; + + for (const team of teams) { + console.log( + `Processing team ${team._id}... Users: ${team.users.length}, Owners: ${team.owners.length}`, + ); + + for (const user of team.users) { + if (!users[user]) { + users[user] = { teams: [], owner: [] }; + } + + users[user].teams.push(team._id.toString()); + } + + for (const user of team.owners) { + if (!users[user]) { + users[user] = { teams: [], owner: [] }; + } + + users[user].owner.push(team._id.toString()); + } + } + + console.log(`Found ${Object.keys(users).length} users who are on teams.`); + + for (const userId in users) { + const user = users[userId]; + + console.log( + `Updating user ${userId}... Teams: ${user.teams.length}, Owners: ${user.owner.length}`, + ); + await db.updateObjectById(CollectionId.Users, new ObjectId(userId), { + teams: user.teams, + owner: user.owner, + }); + } + + process.exit(0); +} + +fixTeamMembership(); From 9504cb13708f7d81946f47b2f489c656b1dec2e9 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 18:13:52 -0500 Subject: [PATCH 53/69] Fix access level errors --- lib/Auth.ts | 1 + lib/DbInterfaceAuthAdapter.ts | 5 ++++- lib/api/AccessLevels.ts | 14 -------------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 1194c470..6963d9f3 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -97,6 +97,7 @@ export const AuthenticationOptions: AuthOptions = { ], callbacks: { async session({ session, user }) { + console.log("Session callback:", session, user); session.user = user; return session; diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index ce361965..4d4a5616 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -258,7 +258,10 @@ export default function DbInterfaceAuthAdapter( return { session: format.from(session), - user: format.from(user), + user: { + ...format.from(user), + _id: user._id, + }, }; }, createSession: async (data: Record) => { diff --git a/lib/api/AccessLevels.ts b/lib/api/AccessLevels.ts index 35e94ba0..df6e5039 100644 --- a/lib/api/AccessLevels.ts +++ b/lib/api/AccessLevels.ts @@ -294,11 +294,6 @@ namespace AccessLevels { pitReportId: string, ) { const user = await userPromise; - console.log( - "[AccessLevels] IfOnTeamThatOwnsPitReport", - pitReportId, - user?._id?.toString(), - ); if (!user) { return { authorized: false, authData: undefined }; } @@ -306,29 +301,20 @@ namespace AccessLevels { const pitReport = await ( await db ).findObjectById(CollectionId.PitReports, new ObjectId(pitReportId)); - console.log("[AccessLevels] pitReport", pitReport); if (!pitReport) { return { authorized: false, authData: undefined }; } const comp = await getCompFromPitReport(await db, pitReport); - console.log("[AccessLevels] comp", comp); if (!comp) { return { authorized: false, authData: undefined }; } const team = await getTeamFromComp(await db, comp); - console.log("[AccessLevels] team", team); if (!team) { return { authorized: false, authData: undefined }; } - console.log( - "[AccessLevels] team.users", - team.users, - user._id?.toString(), - team?.users.includes(user._id?.toString()!), - ); return { authorized: team?.users.includes(user._id?.toString()!), authData: { team, comp, pitReport }, From 2b6565fc391471765e4c12bd0a032d64fe41187e Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 18:14:07 -0500 Subject: [PATCH 54/69] Less logs in Auth --- lib/Auth.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 6963d9f3..1194c470 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -97,7 +97,6 @@ export const AuthenticationOptions: AuthOptions = { ], callbacks: { async session({ session, user }) { - console.log("Session callback:", session, user); session.user = user; return session; From 45919e7a88b7d322aad64bbea6d465bb1c04c009 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 27 Feb 2025 18:38:15 -0500 Subject: [PATCH 55/69] New accounts work --- lib/Auth.ts | 2 ++ lib/DbInterfaceAuthAdapter.ts | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/Auth.ts b/lib/Auth.ts index 1194c470..80bccd19 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -62,6 +62,8 @@ export const AuthenticationOptions: AuthOptions = { ).findObject(CollectionId.Users, { email: profile.email }); if (existing) { + existing.id = profile.sub; + console.log("Found existing user:", existing); return existing; } diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts index 4d4a5616..8077043c 100644 --- a/lib/DbInterfaceAuthAdapter.ts +++ b/lib/DbInterfaceAuthAdapter.ts @@ -43,6 +43,8 @@ export default function DbInterfaceAuthAdapter( return format.from(existingUser); } + logger.debug("Creating user:", adapterUser); + const user = new User( adapterUser.name ?? "Unknown", adapterUser.email, @@ -55,15 +57,16 @@ export default function DbInterfaceAuthAdapter( ), [], [], - undefined, + adapterUser.id, 0, 1, ); user._id = new ObjectId(adapterUser._id) as any; - await db.addObject(CollectionId.Users, user); - return format.from(adapterUser); + const dbUser = await db.addObject(CollectionId.Users, user); + logger.info("Created user:", dbUser._id?.toString()); + return format.from(dbUser); }, getUser: async (id: string) => { const db = await dbPromise; From a37b2881a9109453388d58df8e432a7aa9030a18 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Fri, 28 Feb 2025 15:46:03 -0500 Subject: [PATCH 56/69] Add comp log --- pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx index 9a40bc17..1d429817 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx @@ -397,6 +397,10 @@ export default function CompetitionIndex({ api.remindSlack(team._id.toString(), userId); } + useEffect(() => { + console.log("Comp:", comp); + }, [comp]); + return ( Date: Fri, 28 Feb 2025 15:54:56 -0500 Subject: [PATCH 57/69] Add match log --- components/competition/MatchScheduleCard.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/competition/MatchScheduleCard.tsx b/components/competition/MatchScheduleCard.tsx index 21f12dcf..987e52a9 100644 --- a/components/competition/MatchScheduleCard.tsx +++ b/components/competition/MatchScheduleCard.tsx @@ -14,6 +14,7 @@ import { MdErrorOutline } from "react-icons/md"; import Avatar from "../Avatar"; import Loading from "../Loading"; import { AdvancedSession } from "@/lib/client/useCurrentSession"; +import { useEffect } from "react"; export default function MatchScheduleCard(props: { team: Team | undefined; @@ -68,6 +69,10 @@ export default function MatchScheduleCard(props: { match.reports.some((reportId) => !reportsById[reportId]?.submitted), ); + useEffect(() => { + console.log("Matches", matches); + }, [matches]); + return (
From e34b85f1cfa6fd9344379e45e50b09271a1437ab Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Fri, 28 Feb 2025 16:31:18 -0500 Subject: [PATCH 58/69] Use match number from TBA to set match number in document --- components/competition/MatchScheduleCard.tsx | 15 +++--- lib/TheBlueAlliance.ts | 2 +- tests/lib/DbInterfaceAuthAdapter.test.ts | 55 +++++++++++++++++++- 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/components/competition/MatchScheduleCard.tsx b/components/competition/MatchScheduleCard.tsx index 987e52a9..ca5da383 100644 --- a/components/competition/MatchScheduleCard.tsx +++ b/components/competition/MatchScheduleCard.tsx @@ -63,11 +63,14 @@ export default function MatchScheduleCard(props: { showSubmittedMatches, } = props; - const displayedMatches = showSubmittedMatches - ? matches - : matches.filter((match) => - match.reports.some((reportId) => !reportsById[reportId]?.submitted), - ); + const unsubmittedMatches: Match[] = []; + + for (const match of matches) { + if (match.reports.some((reportId) => !reportsById[reportId]?.submitted)) + unsubmittedMatches.push(match); + } + + const displayedMatches = showSubmittedMatches ? matches : unsubmittedMatches; useEffect(() => { console.log("Matches", matches); @@ -87,7 +90,7 @@ export default function MatchScheduleCard(props: { {isManager && matchesAssigned === false && Object.keys(usersById).length >= 6 ? ( - matchesAssigned !== undefined ? ( + matchesAssigned ? (
{!assigningMatches diff --git a/lib/TheBlueAlliance.ts b/lib/TheBlueAlliance.ts index deb03646..c5c3baf9 100644 --- a/lib/TheBlueAlliance.ts +++ b/lib/TheBlueAlliance.ts @@ -266,7 +266,7 @@ export namespace TheBlueAlliance { .filter((match) => match.comp_level === CompetitionLevel.QM) .map((data, index) => { return new Match( - index + 1, + data.match_number, undefined, data.key, data.time, diff --git a/tests/lib/DbInterfaceAuthAdapter.test.ts b/tests/lib/DbInterfaceAuthAdapter.test.ts index a76bef4d..df7b3ce9 100644 --- a/tests/lib/DbInterfaceAuthAdapter.test.ts +++ b/tests/lib/DbInterfaceAuthAdapter.test.ts @@ -7,8 +7,6 @@ import { get } from "http"; const prototype = DbInterfaceAuthAdapter(undefined as any); -async function getDatabase() {} - async function getAdapterAndDb() { const db = new InMemoryDbInterface(); await db.init(); @@ -80,6 +78,21 @@ describe(prototype.createUser.name, () => { expect(foundUser?.name).toBeDefined(); expect(foundUser?.image).toBeDefined(); }); + + test("Does not create a new user if one already exists", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + email: "test@gmail.com", + }; + + await adapter.createUser(user); + await adapter.createUser(user); + + expect( + await db.countObjects(CollectionId.Users, { email: user.email }), + ).toBe(1); + }); }); describe(prototype.getUser!.name, () => { @@ -118,3 +131,41 @@ describe(prototype.getUser!.name, () => { expect(foundUser).toBeNull(); }); }); + +describe(prototype.getUserByEmail!.name, () => { + test("Returns a user from the database", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + name: "Test User", + email: "test@gmail.com", + }; + + const { _id, ...addedUser } = await db.addObject( + CollectionId.Users, + user as any, + ); + + const foundUser = await adapter.getUserByEmail!(user.email); + + expect(foundUser).toMatchObject(addedUser); + }); + + test("Returns user without their _id", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + _id: new ObjectId(), + name: "Test User", + email: "test@gmail.com", + }; + + await db.addObject(CollectionId.Users, user as any); + + const foundUser = await adapter.getUserByEmail!(user.email); + + const { _id, ...userWithoutId } = user; + + expect(foundUser).toMatchObject(userWithoutId); + }); +}); From 3e02832c216ece7e30ddd66a00a52e7765e16667 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Fri, 28 Feb 2025 16:40:56 -0500 Subject: [PATCH 59/69] Less logs and better code for visualizing scouter assignment --- components/competition/MatchScheduleCard.tsx | 46 ++++++------------- .../[seasonSlug]/[competitonSlug]/index.tsx | 4 -- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/components/competition/MatchScheduleCard.tsx b/components/competition/MatchScheduleCard.tsx index ca5da383..f58876fc 100644 --- a/components/competition/MatchScheduleCard.tsx +++ b/components/competition/MatchScheduleCard.tsx @@ -72,10 +72,6 @@ export default function MatchScheduleCard(props: { const displayedMatches = showSubmittedMatches ? matches : unsubmittedMatches; - useEffect(() => { - console.log("Matches", matches); - }, [matches]); - return (
@@ -88,38 +84,24 @@ export default function MatchScheduleCard(props: { )} {isManager && - matchesAssigned === false && - Object.keys(usersById).length >= 6 ? ( - matchesAssigned ? ( + matchesAssigned === false && + Object.keys(usersById).length >= 6 && + (!assigningMatches ? (
-
- {!assigningMatches - ? "Matches are not assigned" - : "Assigning matches"} -
- {!assigningMatches ? ( - - ) : ( - - )} +
Matches are not assigned.
+
) : ( - ) - ) : ( - <> - )} + ))}
{loadingMatches || loadingReports || loadingUsers ? (
diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx index 1d429817..9a40bc17 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx @@ -397,10 +397,6 @@ export default function CompetitionIndex({ api.remindSlack(team._id.toString(), userId); } - useEffect(() => { - console.log("Comp:", comp); - }, [comp]); - return ( Date: Fri, 28 Feb 2025 17:00:28 -0500 Subject: [PATCH 60/69] Don't allow russian emails --- lib/Auth.ts | 31 +++++++++++++++++++++++-------- lib/AuthUtils.ts | 5 +++++ 2 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 lib/AuthUtils.ts diff --git a/lib/Auth.ts b/lib/Auth.ts index 80bccd19..481fa154 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -12,12 +12,25 @@ import CollectionId from "./client/CollectionId"; import { AdapterUser } from "next-auth/adapters"; import DbInterfaceAuthAdapter from "./DbInterfaceAuthAdapter"; import Logger from "./client/Logger"; +import { allowEmailSignIn } from "./AuthUtils"; const logger = new Logger(["AUTH"], true); const cachedDb = getDatabase(); const adapter = DbInterfaceAuthAdapter(cachedDb); +const email = Email({ + server: { + host: process.env.SMTP_HOST, + port: process.env.SMTP_PORT, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD, + }, + }, + from: process.env.SMTP_FROM, +}); + export const AuthenticationOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, providers: [ @@ -86,15 +99,17 @@ export const AuthenticationOptions: AuthOptions = { }, }), Email({ - server: { - host: process.env.SMTP_HOST, - port: process.env.SMTP_PORT, - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASSWORD, - }, + ...email, + sendVerificationRequest: async (data) => { + const { identifier } = data; + + if (!allowEmailSignIn(identifier)) { + logger.warn("User is not allowed to sign in with email:", identifier); + return; + } + + email.sendVerificationRequest(data); }, - from: process.env.SMTP_FROM, }), ], callbacks: { diff --git a/lib/AuthUtils.ts b/lib/AuthUtils.ts new file mode 100644 index 00000000..356acac6 --- /dev/null +++ b/lib/AuthUtils.ts @@ -0,0 +1,5 @@ +export function allowEmailSignIn(email: string) { + if (email.endsWith(".ru")) return false; + + return true; +} From f226226dc58e8aa533c24835d9af4e12df49bd1e Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Fri, 28 Feb 2025 17:43:27 -0500 Subject: [PATCH 61/69] Add captcha --- environment.d.ts | 3 + lib/Auth.ts | 2 +- package-lock.json | 13 ++++ package.json | 1 + pages/_app.tsx | 73 +++++++++++----------- pages/api/auth/[...nextauth].ts | 33 +++++++++- pages/signin.tsx | 105 +++++++++++++++++++++++++++++--- 7 files changed, 184 insertions(+), 46 deletions(-) diff --git a/environment.d.ts b/environment.d.ts index 4b552d1e..ca36c2f7 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -49,6 +49,9 @@ declare global { DEVELOPER_EMAILS: string; + NEXT_PUBLIC_RECAPTCHA_KEY: string; + RECAPTCHA_SECRET: string; + NODE_ENV: "development" | "production"; } } diff --git a/lib/Auth.ts b/lib/Auth.ts index 481fa154..f827bcf1 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -183,7 +183,7 @@ export const AuthenticationOptions: AuthOptions = { }, }, pages: { - //signIn: "/signin", + signIn: "/signin", }, }; diff --git a/package-lock.json b/package-lock.json index 346c0995..5daa24a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.3.1", "react-ga4": "^2.1.0", + "react-google-recaptcha-v3": "^1.10.1", "react-hot-toast": "^2.5.1", "react-icons": "^5.4.0", "react-p5": "^1.4.1", @@ -9115,6 +9116,18 @@ "resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz", "integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==" }, + "node_modules/react-google-recaptcha-v3": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.10.1.tgz", + "integrity": "sha512-K3AYzSE0SasTn+XvV2tq+6YaxM+zQypk9rbCgG4OVUt7Rh4ze9basIKefoBz9sC0CNslJj9N1uwTTgRMJQbQJQ==", + "dependencies": { + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "react": "^16.3 || ^17.0 || ^18.0", + "react-dom": "^17.0 || ^18.0" + } + }, "node_modules/react-hot-toast": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.1.tgz", diff --git a/package.json b/package.json index d6f4a5de..fa030253 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.3.1", "react-ga4": "^2.1.0", + "react-google-recaptcha-v3": "^1.10.1", "react-hot-toast": "^2.5.1", "react-icons": "^5.4.0", "react-p5": "^1.4.1", diff --git a/pages/_app.tsx b/pages/_app.tsx index a93eeca6..84be4d03 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -14,6 +14,7 @@ import { Toaster } from "react-hot-toast"; import resolveConfig from "tailwindcss/resolveConfig"; import tailwindConfig from "../tailwind.config.js"; import Head from "next/head"; +import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3"; const tailwind = resolveConfig(tailwindConfig); @@ -35,41 +36,45 @@ export default function App({ href="/manifest.json" /> - - - + + + + - - - - + }} + /> + + + + ); } diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 1aa2db6f..888b1510 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -1,7 +1,38 @@ import { NextApiRequest, NextApiResponse } from "next"; import Auth from "@/lib/Auth"; -function getAuth(req: NextApiRequest, res: NextApiResponse) { +async function getAuth(req: NextApiRequest, res: NextApiResponse) { + const path = [ + "", + ...(Array.isArray(req.query.nextauth) + ? req.query.nextauth + : [req.query.nextauth]), + ].join("/"); + + if (path === "/signin/email" && process.env.RECAPTCHA_SECRET) { + const { email, captchaToken } = req.body; + const isHuman = await fetch( + `https://www.google.com/recaptcha/api/siteverify`, + { + method: "post", + headers: { + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + }, + body: `secret=${process.env.RECAPTCHA_SECRET}&response=${captchaToken}`, + }, + ) + .then((res) => res.json()) + .then((json) => json.success) + .catch((err) => { + throw new Error(`Error in Google Siteverify API. ${err.message}`); + }); + console.log("IS HUMAN", isHuman, email); + if (!isHuman) { + res.status(400).end(); + return; + } + } return Auth(req, res); } diff --git a/pages/signin.tsx b/pages/signin.tsx index ac60a004..a9b74155 100644 --- a/pages/signin.tsx +++ b/pages/signin.tsx @@ -1,23 +1,108 @@ +import { signIn } from "next-auth/react"; +import { useRouter } from "next/router"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { + GoogleReCaptchaProvider, + useGoogleReCaptcha, +} from "react-google-recaptcha-v3"; import { FaGoogle, FaSlack } from "react-icons/fa"; export default function SignIn() { + const router = useRouter(); + const emailRef = useRef(null); + const { executeRecaptcha } = useGoogleReCaptcha(); + + const [error, setError] = useState(router.query.error as string); + const [captchaToken, setCaptchaToken] = useState(); + + useEffect(() => { + if (router.query.error) { + setError(router.query.error as string); + } + }, [router.query.error]); + + function signInWithCallbackUrl(provider: string, options?: object) { + const callbackUrl = router.query.callbackUrl as string; + + signIn(provider, { callbackUrl, ...options }); + } + + function logInWithEmail() { + const email = emailRef.current?.value; + + if (!email) { + setError("Email is required"); + return; + } + + if (!captchaToken) { + setError("Please verify you are human"); + return; + } + + signInWithCallbackUrl("email", { email, captchaToken }); + } + + const verifyCaptcha = useCallback(async () => { + if (!executeRecaptcha) { + setError("Recaptcha not available"); + return; + } + + const token = await executeRecaptcha("login"); + setCaptchaToken(token); + }, [executeRecaptcha]); + return (

Sign In

-

Choose a login provider

-
- - - + {error &&

{error}

} +

Choose a login provider

+
-

For Team 4026 Only:

- + + + +
+
+

Email Sign In

+ + {!captchaToken && ( + + )} + + {captchaToken &&

Captcha verified!

} +
From dcf23723d49d44cc5cebcc5cce68a2d16ac2a1de Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Fri, 28 Feb 2025 17:45:54 -0500 Subject: [PATCH 62/69] Add log for blocked emails --- pages/api/auth/[...nextauth].ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 888b1510..728e4600 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -27,9 +27,11 @@ async function getAuth(req: NextApiRequest, res: NextApiResponse) { .catch((err) => { throw new Error(`Error in Google Siteverify API. ${err.message}`); }); - console.log("IS HUMAN", isHuman, email); + if (!isHuman) { res.status(400).end(); + + console.log("User is not human:", email); return; } } From acbb20fa8a19c072e2f47b8b3442048095975203 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Fri, 28 Feb 2025 17:51:52 -0500 Subject: [PATCH 63/69] Link to sign in page in header --- components/Container.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Container.tsx b/components/Container.tsx index a3ee606d..b199461e 100644 --- a/components/Container.tsx +++ b/components/Container.tsx @@ -231,7 +231,7 @@ export default function Container(props: ContainerProps) { ) : ( From 6dec8c1ac807758456aeeeecc0b80a1b9663d8a1 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Fri, 28 Feb 2025 17:52:30 -0500 Subject: [PATCH 64/69] Also link to sign in --- components/Container.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Container.tsx b/components/Container.tsx index b199461e..64b1a3aa 100644 --- a/components/Container.tsx +++ b/components/Container.tsx @@ -268,7 +268,7 @@ export default function Container(props: ContainerProps) {

Wait a minute...

You need to sign in first!

- +
From 89ffd9baa0021862c9bc3477255b389033b0c397 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Fri, 28 Feb 2025 21:13:30 -0500 Subject: [PATCH 65/69] Add log for recaptcha key --- index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.ts b/index.ts index 1414cd66..3cbe01e8 100644 --- a/index.ts +++ b/index.ts @@ -16,6 +16,8 @@ const dev = process.env.NODE_ENV !== "production"; console.log("Constants set"); +console.log("Recaptcha key: " + process.env.NEXT_PUBLIC_RECAPTCHA_KEY); + const useHttps = existsSync("./certs/key.pem") && existsSync("./certs/cert.pem"); From eaf81a352b92531c7fb8fc91654acd59c0e29e39 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Fri, 28 Feb 2025 21:30:46 -0500 Subject: [PATCH 66/69] Add captcha key to production public env --- .env.production | 3 ++- index.ts | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.env.production b/.env.production index c9800a26..4bc8765d 100644 --- a/.env.production +++ b/.env.production @@ -2,4 +2,5 @@ # We have to include these at build time, so this file is used to inject them into the build process. NEXT_PUBLIC_API_URL=/api/ NEXT_PUBLIC_SLACK_CLIENT_ID=10831824934.7404945710466 -NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-1BFJYBDC76 \ No newline at end of file +NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-1BFJYBDC76 +NEXT_PUBLIC_RECAPTCHA_KEY=6Le63OUqAAAAABxxDrbaU9OywDLLHqutVwbw7a9d \ No newline at end of file diff --git a/index.ts b/index.ts index 3cbe01e8..1414cd66 100644 --- a/index.ts +++ b/index.ts @@ -16,8 +16,6 @@ const dev = process.env.NODE_ENV !== "production"; console.log("Constants set"); -console.log("Recaptcha key: " + process.env.NEXT_PUBLIC_RECAPTCHA_KEY); - const useHttps = existsSync("./certs/key.pem") && existsSync("./certs/cert.pem"); From 66df04fc0487d965336d833b9929405914e90d82 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Fri, 28 Feb 2025 21:43:45 -0500 Subject: [PATCH 67/69] Clean up Recaptcha setup --- components/Container.tsx | 5 -- pages/_app.tsx | 72 ++++++++++++------------ pages/signin.tsx | 117 +++++++++++++++++++-------------------- 3 files changed, 91 insertions(+), 103 deletions(-) diff --git a/components/Container.tsx b/components/Container.tsx index 64b1a3aa..e3c29d1c 100644 --- a/components/Container.tsx +++ b/components/Container.tsx @@ -74,11 +74,6 @@ export default function Container(props: ContainerProps) { }, [eventSearch]); useEffect(() => { - if (window.location.href.includes("signin")) { - console.log("triggered"); - location.reload(); - } - const loadTeams = async () => { if (!user) { return; diff --git a/pages/_app.tsx b/pages/_app.tsx index 84be4d03..af3a43c7 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -36,45 +36,41 @@ export default function App({ href="/manifest.json" /> - - - - - + + - - - - + ], + siteName: "Gearbox", + }} + /> + + + + ); } diff --git a/pages/signin.tsx b/pages/signin.tsx index a9b74155..7c618200 100644 --- a/pages/signin.tsx +++ b/pages/signin.tsx @@ -1,3 +1,4 @@ +import Container from "@/components/Container"; import { signIn } from "next-auth/react"; import { useRouter } from "next/router"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -7,13 +8,12 @@ import { } from "react-google-recaptcha-v3"; import { FaGoogle, FaSlack } from "react-icons/fa"; -export default function SignIn() { +function SignInCard() { const router = useRouter(); const emailRef = useRef(null); const { executeRecaptcha } = useGoogleReCaptcha(); const [error, setError] = useState(router.query.error as string); - const [captchaToken, setCaptchaToken] = useState(); useEffect(() => { if (router.query.error) { @@ -26,8 +26,7 @@ export default function SignIn() { signIn(provider, { callbackUrl, ...options }); } - - function logInWithEmail() { + async function logInWithEmail() { const email = emailRef.current?.value; if (!email) { @@ -35,76 +34,74 @@ export default function SignIn() { return; } - if (!captchaToken) { - setError("Please verify you are human"); - return; - } - - signInWithCallbackUrl("email", { email, captchaToken }); - } - - const verifyCaptcha = useCallback(async () => { if (!executeRecaptcha) { setError("Recaptcha not available"); return; } - const token = await executeRecaptcha("login"); - setCaptchaToken(token); - }, [executeRecaptcha]); + const captchaToken = await executeRecaptcha(); + + signInWithCallbackUrl("email", { email, captchaToken }); + } return ( -
-
-
-

Sign In

- {error &&

{error}

} -

Choose a login provider

-
+
+
+

Sign In

+ {error &&

{error}

} +

Choose a login provider

+
- + + + +
+
+

Email Sign In

+ - -
-
-

Email Sign In

- - {!captchaToken && ( - - )} - - {captchaToken &&

Captcha verified!

} -
); } + +export default function SignIn() { + return ( + + +
+ +
+
+
+ ); +} From 167c40ac18683b5e955e8ad5bb1109a38e5ffb52 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Fri, 28 Feb 2025 21:54:12 -0500 Subject: [PATCH 68/69] Remove old email blacklist --- lib/Auth.ts | 31 ++++++++----------------------- lib/AuthUtils.ts | 5 ----- 2 files changed, 8 insertions(+), 28 deletions(-) delete mode 100644 lib/AuthUtils.ts diff --git a/lib/Auth.ts b/lib/Auth.ts index f827bcf1..5f7fdfef 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -12,25 +12,12 @@ import CollectionId from "./client/CollectionId"; import { AdapterUser } from "next-auth/adapters"; import DbInterfaceAuthAdapter from "./DbInterfaceAuthAdapter"; import Logger from "./client/Logger"; -import { allowEmailSignIn } from "./AuthUtils"; const logger = new Logger(["AUTH"], true); const cachedDb = getDatabase(); const adapter = DbInterfaceAuthAdapter(cachedDb); -const email = Email({ - server: { - host: process.env.SMTP_HOST, - port: process.env.SMTP_PORT, - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASSWORD, - }, - }, - from: process.env.SMTP_FROM, -}); - export const AuthenticationOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, providers: [ @@ -99,17 +86,15 @@ export const AuthenticationOptions: AuthOptions = { }, }), Email({ - ...email, - sendVerificationRequest: async (data) => { - const { identifier } = data; - - if (!allowEmailSignIn(identifier)) { - logger.warn("User is not allowed to sign in with email:", identifier); - return; - } - - email.sendVerificationRequest(data); + server: { + host: process.env.SMTP_HOST, + port: process.env.SMTP_PORT, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD, + }, }, + from: process.env.SMTP_FROM, }), ], callbacks: { diff --git a/lib/AuthUtils.ts b/lib/AuthUtils.ts deleted file mode 100644 index 356acac6..00000000 --- a/lib/AuthUtils.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function allowEmailSignIn(email: string) { - if (email.endsWith(".ru")) return false; - - return true; -} From 1fa4a4251a04da9e76128e37c8e5850f5d667d82 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Sat, 1 Mar 2025 14:38:06 -0500 Subject: [PATCH 69/69] Use Logger in index.ts --- index.ts | 27 ++++++++++++++++----------- package.json | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/index.ts b/index.ts index 1414cd66..1ba75545 100644 --- a/index.ts +++ b/index.ts @@ -9,12 +9,15 @@ import { request, createServer as createServerHttp, } from "http"; +import Logger from "./lib/client/Logger"; -console.log("Starting server..."); +const logger = new Logger(["STARTUP"]); + +logger.log("Starting server..."); const dev = process.env.NODE_ENV !== "production"; -console.log("Constants set"); +logger.debug("Constants set"); const useHttps = existsSync("./certs/key.pem") && existsSync("./certs/cert.pem"); @@ -27,26 +30,28 @@ const httpsOptions = useHttps : {}; const port = useHttps ? 443 : 80; -console.log(`Using port ${port}`); +logger.debug(`Using port ${port}`); const app = next({ dev, port }); const handle = app.getRequestHandler(); -console.log("App preparing..."); +logger.debug("App preparing..."); app.prepare().then(() => { - console.log("App prepared. Creating server..."); + logger.debug("App prepared. Creating server..."); + + const ioLogger = new Logger(["NETWORKIO"]); async function handleRaw( req: IncomingMessage, res: ServerResponse, ) { const start = Date.now(); - console.log(`IN: ${req.method} ${req.url}`); + ioLogger.debug(`IN: ${req.method} ${req.url}`); if (!req.url) return; const parsedUrl = parse(req.url, true); handle(req, res, parsedUrl).then(() => - console.log( + ioLogger.debug( `OUT: ${req.method} ${req.url} ${res.statusCode} in ${Date.now() - start}ms`, ), ); @@ -59,20 +64,20 @@ app.prepare().then(() => { : createServerHttp(handleRaw) ) .listen(port, () => { - console.log( + logger.info( process.env.NODE_ENV + ` Server Running At: ${useHttps ? "https" : "http"}://localhost:` + port, ); }) .on("error", (err: Error) => { - console.log(err); + logger.error(err); throw err; }); - console.log("Server created. Listening: " + server.listening); + logger.debug("Server created. Listening: " + server.listening); } catch (err) { - console.log(err); + logger.error(err); throw err; } }); diff --git a/package.json b/package.json index fa030253..93bc53bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.5", + "version": "1.2.6", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0",