Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 87 additions & 3 deletions src/pages/api/listings/details/[slug].ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,72 @@ import logger from '@/lib/logger';
import { prisma } from '@/prisma';
import { convertDatesToISO, safeStringify } from '@/utils/safeStringify';

export async function getListingDetailsBySlug(slug: string): Promise<any> {
import { getPrivyToken } from '@/features/auth/utils/getPrivyToken';

interface ListingWithProFlag {
readonly isPro?: boolean;
// The following fields are intentionally loose, as they are
// coming directly from Prisma without a strict type.
description?: string | null;
eligibility?: unknown;
requirements?: string | null;
commitmentDate?: string | null;
applicationLink?: string | null;
references?: unknown;
Hackathon?: {
description?: string | null;
eligibility?: unknown;
// Allow any additional hackathon fields without typing them here.

[key: string]: any;
} | null;
// Allow any additional listing fields without typing them here.

[key: string]: any;
}

const SENSITIVE_LISTING_FIELDS: ReadonlyArray<keyof ListingWithProFlag> = [
'description',
'eligibility',
'requirements',
'commitmentDate',
'applicationLink',
'references',
];

const sanitizeListingForViewer = <TListing extends ListingWithProFlag>(
listing: TListing | null,
viewerIsPro: boolean,
): TListing | null => {
if (!listing || listing.isPro !== true || viewerIsPro) {
return listing;
}

const sanitizedListing: ListingWithProFlag = { ...listing };

for (const field of SENSITIVE_LISTING_FIELDS) {
if (field in sanitizedListing) {
// We prefer `null` over `undefined` so JSON shape remains stable.

(sanitizedListing as any)[field] = null;
}
}

if (sanitizedListing.Hackathon) {
sanitizedListing.Hackathon = {
...sanitizedListing.Hackathon,
description: null,
eligibility: null,
};
}

return sanitizedListing as TListing;
};

export async function getListingDetailsBySlug(
slug: string,
options?: { viewerIsPro?: boolean },
): Promise<any> {
if (!slug) {
throw new Error('Missing required query parameters: slug');
}
Expand Down Expand Up @@ -58,7 +123,13 @@ export async function getListingDetailsBySlug(slug: string): Promise<any> {
},
});

return convertDatesToISO(result);
const listingWithIsoDates = convertDatesToISO(
result,
) as ListingWithProFlag | null;

const viewerIsPro = options?.viewerIsPro ?? false;

return sanitizeListingForViewer(listingWithIsoDates, viewerIsPro);
}

export default async function handler(
Expand All @@ -78,7 +149,20 @@ export default async function handler(
}

try {
const result = await getListingDetailsBySlug(slug);
const privyDid = await getPrivyToken(req);

let viewerIsPro = false;

if (privyDid) {
const viewer = await prisma.user.findUnique({
where: { privyDid },
select: { isPro: true },
});

viewerIsPro = viewer?.isPro ?? false;
}

const result = await getListingDetailsBySlug(slug, { viewerIsPro });

if (!result) {
logger.warn(`Bounty with slug=${slug} not found`);
Expand Down
23 changes: 20 additions & 3 deletions src/pages/listing/[slug]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import { ExternalImage } from '@/components/ui/cloudinary-image';
import { ListingPageLayout } from '@/layouts/Listing';
import { getSubmissionCount } from '@/pages/api/listings/[listingId]/submission-count';
import { getListingDetailsBySlug } from '@/pages/api/listings/details/[slug]';
import { prisma } from '@/prisma';
import {
generateBreadcrumbListSchema,
generateJobPostingSchema,
} from '@/utils/json-ld';

import { getPrivyToken } from '@/features/auth/utils/getPrivyToken';
import { ListingPop } from '@/features/conversion-popups/components/ListingPop';
import { DescriptionUI } from '@/features/listings/components/ListingPage/DescriptionUI';
import { ListingWinners } from '@/features/listings/components/ListingPage/ListingWinners';
Expand Down Expand Up @@ -84,10 +86,25 @@ function ListingDetails({
}

export const getServerSideProps: GetServerSideProps = async (context) => {
const { slug } = context.query;
let listingData;
const slug = Array.isArray(context.query.slug)
? context.query.slug[0]
: context.query.slug;
let listingData: Awaited<ReturnType<typeof getListingDetailsBySlug>> | null;
try {
listingData = await getListingDetailsBySlug(String(slug));
const privyDid = await getPrivyToken(context.req);

let viewerIsPro = false;

if (privyDid) {
const viewer = await prisma.user.findUnique({
where: { privyDid },
select: { isPro: true },
});

viewerIsPro = viewer?.isPro ?? false;
}

listingData = await getListingDetailsBySlug(String(slug), { viewerIsPro });
} catch (e) {
console.error(e);
listingData = null;
Expand Down
61 changes: 40 additions & 21 deletions src/pages/listing/[slug]/winner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { useEffect, useState } from 'react';
import type { SubmissionWithUser } from '@/interface/submission';
import { getWinningSubmissionsByListingId } from '@/pages/api/listings/[listingId]/winners';
import { getListingDetailsBySlug } from '@/pages/api/listings/details/[slug]';
import { prisma } from '@/prisma';
import { sortRank } from '@/utils/rank';
import { getURL } from '@/utils/validUrl';

import { getPrivyToken } from '@/features/auth/utils/getPrivyToken';
import { BONUS_REWARD_POSITION } from '@/features/listing-builder/constants';
import { type Listing, type Rewards } from '@/features/listings/types';
import { getListingTypeLabel } from '@/features/listings/utils/status';
Expand Down Expand Up @@ -96,7 +98,9 @@ interface StrippedSubmission {
}

export const getServerSideProps: GetServerSideProps = async (context) => {
const { slug } = context.query;
const slug = Array.isArray(context.query.slug)
? context.query.slug[0]
: context.query.slug;
const { req } = context;
const protocol = req.headers['x-forwarded-proto'] || 'http';
const host = req.headers.host;
Expand All @@ -105,27 +109,42 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
let bountyData;
const submissions: StrippedSubmission[] = [];
try {
bountyData = await getListingDetailsBySlug(String(slug));

let data = await getWinningSubmissionsByListingId(String(bountyData.id));
data = data.filter((d) => d.winnerPosition !== BONUS_REWARD_POSITION);
const winners = sortRank(
data.map((submission) => submission.winnerPosition || NaN),
);
const sortedSubmissions = winners.map((position) =>
data.find((d: SubmissionWithUser) => d.winnerPosition === position),
) as SubmissionWithUser[];
sortedSubmissions.forEach((s) => {
submissions.push({
id: s.id,
winnerPosition: s.winnerPosition,
user: {
firstName: s.user.firstName,
lastName: s.user.lastName,
photo: s.user.photo,
},
const privyDid = await getPrivyToken(req);

let viewerIsPro = false;

if (privyDid) {
const viewer = await prisma.user.findUnique({
where: { privyDid },
select: { isPro: true },
});

viewerIsPro = viewer?.isPro ?? false;
}

bountyData = await getListingDetailsBySlug(String(slug), { viewerIsPro });

if (bountyData?.id) {
let data = await getWinningSubmissionsByListingId(String(bountyData.id));
data = data.filter((d) => d.winnerPosition !== BONUS_REWARD_POSITION);
const winners = sortRank(
data.map((submission) => submission.winnerPosition || NaN),
);
const sortedSubmissions = winners.map((position) =>
data.find((d: SubmissionWithUser) => d.winnerPosition === position),
) as SubmissionWithUser[];
sortedSubmissions.forEach((s) => {
submissions.push({
id: s.id,
winnerPosition: s.winnerPosition,
user: {
firstName: s.user.firstName,
lastName: s.user.lastName,
photo: s.user.photo,
},
});
});
});
}
} catch (e) {
console.error(e);
bountyData = null;
Expand Down