diff --git a/web/pages/api/v0/_types.ts b/web/pages/api/v0/_types.ts new file mode 100644 index 00000000..5b9a7dab --- /dev/null +++ b/web/pages/api/v0/_types.ts @@ -0,0 +1,230 @@ +import { Bet } from 'common/bet' +import { Answer } from 'common/answer' +import { getOutcomeProbability, getProbability } from 'common/calculate' +import { Comment } from 'common/comment' +import { Contract } from 'common/contract' +import { User } from 'common/user' +import { removeUndefinedProps } from 'common/util/object' +import { ENV_CONFIG } from 'common/envs/constants' +import { JSONContent } from '@tiptap/core' + +export type LiteMarket = { + // Unique identifer for this market + id: string + + // Attributes about the creator + creatorUsername: string + creatorName: string + createdTime: number + creatorAvatarUrl?: string + + // Market attributes. All times are in milliseconds since epoch + closeTime?: number + question: string + description: string | JSONContent + tags: string[] + url: string + outcomeType: string + mechanism: string + + pool: { [outcome: string]: number } + probability?: number + p?: number + totalLiquidity?: number + + volume: number + volume7Days: number + volume24Hours: number + + isResolved: boolean + resolution?: string + resolutionTime?: number + resolutionProbability?: number +} + +export type ApiAnswer = Answer & { + probability?: number +} + +export type FullMarket = LiteMarket & { + bets: Bet[] + comments: Comment[] + answers?: ApiAnswer[] +} + +export type ApiError = { + error: string +} + +type ValidationErrorDetail = { + field: string | null + error: string +} +export class ValidationError { + details: ValidationErrorDetail[] + + constructor(details: ValidationErrorDetail[]) { + this.details = details + } +} + +export function toLiteMarket(contract: Contract): LiteMarket { + const { + id, + creatorUsername, + creatorName, + createdTime, + creatorAvatarUrl, + closeTime, + question, + description, + tags, + slug, + pool, + outcomeType, + mechanism, + volume, + volume7Days, + volume24Hours, + isResolved, + resolution, + resolutionTime, + resolutionProbability, + } = contract + + const { p, totalLiquidity } = contract as any + + const probability = + contract.outcomeType === 'BINARY' ? getProbability(contract) : undefined + + return removeUndefinedProps({ + id, + creatorUsername, + creatorName, + createdTime, + creatorAvatarUrl, + closeTime: + resolutionTime && closeTime + ? Math.min(resolutionTime, closeTime) + : closeTime, + question, + description, + tags, + url: `https://manifold.markets/${creatorUsername}/${slug}`, + pool, + probability, + p, + totalLiquidity, + outcomeType, + mechanism, + volume, + volume7Days, + volume24Hours, + isResolved, + resolution, + resolutionTime, + resolutionProbability, + }) +} + +export function toFullMarket( + contract: Contract, + comments: Comment[], + bets: Bet[] +): FullMarket { + const liteMarket = toLiteMarket(contract) + const answers = + contract.outcomeType === 'FREE_RESPONSE' + ? contract.answers.map((answer) => + augmentAnswerWithProbability(contract, answer) + ) + : undefined + + return { + ...liteMarket, + answers, + comments, + bets, + } +} + +function augmentAnswerWithProbability( + contract: Contract, + answer: Answer +): ApiAnswer { + const probability = getOutcomeProbability(contract, answer.id) + return { + ...answer, + probability, + } +} + +export type LiteUser = { + id: string + createdTime: number + + name: string + username: string + url: string + avatarUrl?: string + + bio?: string + bannerUrl?: string + website?: string + twitterHandle?: string + discordHandle?: string + + balance: number + totalDeposits: number + + profitCached: { + daily: number + weekly: number + monthly: number + allTime: number + } + + creatorVolumeCached: { + daily: number + weekly: number + monthly: number + allTime: number + } +} + +export function toLiteUser(user: User): LiteUser { + const { + id, + createdTime, + name, + username, + avatarUrl, + bio, + bannerUrl, + website, + twitterHandle, + discordHandle, + balance, + totalDeposits, + profitCached, + creatorVolumeCached, + } = user + + return removeUndefinedProps({ + id, + createdTime, + name, + username, + url: `https://${ENV_CONFIG.domain}/${username}`, + avatarUrl, + bio, + bannerUrl, + website, + twitterHandle, + discordHandle, + balance, + totalDeposits, + profitCached, + creatorVolumeCached, + }) +} diff --git a/web/pages/api/v0/_validate.ts b/web/pages/api/v0/_validate.ts new file mode 100644 index 00000000..25f5af4e --- /dev/null +++ b/web/pages/api/v0/_validate.ts @@ -0,0 +1,17 @@ +import { z } from 'zod' +import { ValidationError } from './_types' + +export const validate = (schema: T, val: unknown) => { + const result = schema.safeParse(val) + if (!result.success) { + const issues = result.error.issues.map((i) => { + return { + field: i.path.join('.') || null, + error: i.message, + } + }) + throw new ValidationError(issues) + } else { + return result.data as z.infer + } +} diff --git a/web/pages/api/v0/bet/cancel/[betId].ts b/web/pages/api/v0/bet/cancel/[betId].ts new file mode 100644 index 00000000..878b9349 --- /dev/null +++ b/web/pages/api/v0/bet/cancel/[betId].ts @@ -0,0 +1,27 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { + CORS_ORIGIN_MANIFOLD, + CORS_ORIGIN_LOCALHOST, +} from 'common/envs/constants' +import { applyCorsHeaders } from 'web/lib/api/cors' +import { fetchBackend, forwardResponse } from 'web/lib/api/proxy' + +export const config = { api: { bodyParser: true } } + +export default async function route(req: NextApiRequest, res: NextApiResponse) { + await applyCorsHeaders(req, res, { + origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], + methods: 'POST', + }) + + const { betId } = req.query as { betId: string } + + if (req.body) req.body.betId = betId + try { + const backendRes = await fetchBackend(req, 'cancelbet') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ message: 'Error communicating with backend.' }) + } +} diff --git a/web/pages/api/v0/bet/index.ts b/web/pages/api/v0/bet/index.ts new file mode 100644 index 00000000..ab7d78aa --- /dev/null +++ b/web/pages/api/v0/bet/index.ts @@ -0,0 +1,23 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { + CORS_ORIGIN_MANIFOLD, + CORS_ORIGIN_LOCALHOST, +} from 'common/envs/constants' +import { applyCorsHeaders } from 'web/lib/api/cors' +import { fetchBackend, forwardResponse } from 'web/lib/api/proxy' + +export const config = { api: { bodyParser: false } } + +export default async function route(req: NextApiRequest, res: NextApiResponse) { + await applyCorsHeaders(req, res, { + origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], + methods: 'POST', + }) + try { + const backendRes = await fetchBackend(req, 'placebet') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ message: 'Error communicating with backend.' }) + } +} diff --git a/web/pages/api/v0/bets.ts b/web/pages/api/v0/bets.ts new file mode 100644 index 00000000..c3de3e97 --- /dev/null +++ b/web/pages/api/v0/bets.ts @@ -0,0 +1,66 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { Bet, getBets } from 'web/lib/firebase/bets' +import { getContractFromSlug } from 'web/lib/firebase/contracts' +import { getUserByUsername } from 'web/lib/firebase/users' +import { ApiError, ValidationError } from './_types' +import { z } from 'zod' +import { validate } from './_validate' + +const queryParams = z + .object({ + username: z.string().optional(), + market: z.string().optional(), + limit: z + .number() + .default(1000) + .or(z.string().regex(/\d+/).transform(Number)) + .refine((n) => n >= 0 && n <= 1000, 'Limit must be between 0 and 1000'), + before: z.string().optional(), + }) + .strict() + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + + let params: z.infer + try { + params = validate(queryParams, req.query) + } catch (e) { + if (e instanceof ValidationError) { + return res.status(400).json(e) + } + console.error(`Unknown error during validation: ${e}`) + return res.status(500).json({ error: 'Unknown error during validation' }) + } + + const { username, market, limit, before } = params + + let userId: string | undefined + if (username) { + const user = await getUserByUsername(username) + if (!user) { + res.status(404).json({ error: 'User not found' }) + return + } + userId = user.id + } + + let contractId: string | undefined + if (market) { + const contract = await getContractFromSlug(market) + if (!contract) { + res.status(404).json({ error: 'Contract not found' }) + return + } + contractId = contract.id + } + + const bets = await getBets({ userId, contractId, limit, before }) + + res.setHeader('Cache-Control', 'max-age=0') + return res.status(200).json(bets) +} diff --git a/web/pages/api/v0/group/[slug].ts b/web/pages/api/v0/group/[slug].ts new file mode 100644 index 00000000..f9271591 --- /dev/null +++ b/web/pages/api/v0/group/[slug].ts @@ -0,0 +1,18 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { getGroupBySlug } from 'web/lib/firebase/groups' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { slug } = req.query + const group = await getGroupBySlug(slug as string) + if (!group) { + res.status(404).json({ error: 'Group not found' }) + return + } + res.setHeader('Cache-Control', 'no-cache') + return res.status(200).json(group) +} diff --git a/web/pages/api/v0/group/by-id/[id].ts b/web/pages/api/v0/group/by-id/[id].ts new file mode 100644 index 00000000..3260302b --- /dev/null +++ b/web/pages/api/v0/group/by-id/[id].ts @@ -0,0 +1,18 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { getGroup } from 'web/lib/firebase/groups' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { id } = req.query + const group = await getGroup(id as string) + if (!group) { + res.status(404).json({ error: 'Group not found' }) + return + } + res.setHeader('Cache-Control', 'no-cache') + return res.status(200).json(group) +} diff --git a/web/pages/api/v0/groups.ts b/web/pages/api/v0/groups.ts new file mode 100644 index 00000000..84b773b3 --- /dev/null +++ b/web/pages/api/v0/groups.ts @@ -0,0 +1,15 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import { listAllGroups } from 'web/lib/firebase/groups' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' + +type Data = any[] + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const groups = await listAllGroups() + res.setHeader('Cache-Control', 'max-age=0') + res.status(200).json(groups) +} diff --git a/web/pages/api/v0/market/[id]/index.ts b/web/pages/api/v0/market/[id]/index.ts new file mode 100644 index 00000000..eb238dab --- /dev/null +++ b/web/pages/api/v0/market/[id]/index.ts @@ -0,0 +1,29 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { listAllBets } from 'web/lib/firebase/bets' +import { listAllComments } from 'web/lib/firebase/comments' +import { getContractFromId } from 'web/lib/firebase/contracts' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { FullMarket, ApiError, toFullMarket } from '../../_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { id } = req.query + const contractId = id as string + + const [contract, bets, comments] = await Promise.all([ + getContractFromId(contractId), + listAllBets(contractId), + listAllComments(contractId), + ]) + + if (!contract) { + res.status(404).json({ error: 'Contract not found' }) + return + } + + res.setHeader('Cache-Control', 'max-age=0') + return res.status(200).json(toFullMarket(contract, comments, bets)) +} diff --git a/web/pages/api/v0/market/[id]/lite.ts b/web/pages/api/v0/market/[id]/lite.ts new file mode 100644 index 00000000..7688caa8 --- /dev/null +++ b/web/pages/api/v0/market/[id]/lite.ts @@ -0,0 +1,23 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { getContractFromId } from 'web/lib/firebase/contracts' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { ApiError, toLiteMarket, LiteMarket } from '../../_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { id } = req.query + const contractId = id as string + + const contract = await getContractFromId(contractId) + + if (!contract) { + res.status(404).json({ error: 'Contract not found' }) + return + } + + res.setHeader('Cache-Control', 'max-age=0') + return res.status(200).json(toLiteMarket(contract)) +} diff --git a/web/pages/api/v0/market/[id]/resolve.ts b/web/pages/api/v0/market/[id]/resolve.ts new file mode 100644 index 00000000..1f291288 --- /dev/null +++ b/web/pages/api/v0/market/[id]/resolve.ts @@ -0,0 +1,28 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { + CORS_ORIGIN_MANIFOLD, + CORS_ORIGIN_LOCALHOST, +} from 'common/envs/constants' +import { applyCorsHeaders } from 'web/lib/api/cors' +import { fetchBackend, forwardResponse } from 'web/lib/api/proxy' + +export const config = { api: { bodyParser: true } } + +export default async function route(req: NextApiRequest, res: NextApiResponse) { + await applyCorsHeaders(req, res, { + origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], + methods: 'POST', + }) + + const { id } = req.query + const contractId = id as string + + if (req.body) req.body.contractId = contractId + try { + const backendRes = await fetchBackend(req, 'resolvemarket') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ message: 'Error communicating with backend.' }) + } +} diff --git a/web/pages/api/v0/market/[id]/sell.ts b/web/pages/api/v0/market/[id]/sell.ts new file mode 100644 index 00000000..431121f2 --- /dev/null +++ b/web/pages/api/v0/market/[id]/sell.ts @@ -0,0 +1,28 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { + CORS_ORIGIN_MANIFOLD, + CORS_ORIGIN_LOCALHOST, +} from 'common/envs/constants' +import { applyCorsHeaders } from 'web/lib/api/cors' +import { fetchBackend, forwardResponse } from 'web/lib/api/proxy' + +export const config = { api: { bodyParser: true } } + +export default async function route(req: NextApiRequest, res: NextApiResponse) { + await applyCorsHeaders(req, res, { + origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], + methods: 'POST', + }) + + const { id } = req.query + const contractId = id as string + + if (req.body) req.body.contractId = contractId + try { + const backendRes = await fetchBackend(req, 'sellshares') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ message: 'Error communicating with backend.' }) + } +} diff --git a/web/pages/api/v0/market/index.ts b/web/pages/api/v0/market/index.ts new file mode 100644 index 00000000..c9e82800 --- /dev/null +++ b/web/pages/api/v0/market/index.ts @@ -0,0 +1,23 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { + CORS_ORIGIN_MANIFOLD, + CORS_ORIGIN_LOCALHOST, +} from 'common/envs/constants' +import { applyCorsHeaders } from 'web/lib/api/cors' +import { fetchBackend, forwardResponse } from 'web/lib/api/proxy' + +export const config = { api: { bodyParser: false } } + +export default async function route(req: NextApiRequest, res: NextApiResponse) { + await applyCorsHeaders(req, res, { + origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], + methods: 'POST', + }) + try { + const backendRes = await fetchBackend(req, 'createmarket') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ message: 'Error communicating with backend.' }) + } +} diff --git a/web/pages/api/v0/markets.ts b/web/pages/api/v0/markets.ts new file mode 100644 index 00000000..56ecc594 --- /dev/null +++ b/web/pages/api/v0/markets.ts @@ -0,0 +1,51 @@ +// Next.js API route support: https://vercel.com/docs/concepts/functions/serverless-functions +import type { NextApiRequest, NextApiResponse } from 'next' +import { listAllContracts } from 'web/lib/firebase/contracts' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { toLiteMarket, ValidationError } from './_types' +import { z } from 'zod' +import { validate } from './_validate' + +const queryParams = z + .object({ + limit: z + .number() + .default(1000) + .or(z.string().regex(/\d+/).transform(Number)) + .refine((n) => n >= 0 && n <= 1000, 'Limit must be between 0 and 1000'), + before: z.string().optional(), + }) + .strict() + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + + let params: z.infer + try { + params = validate(queryParams, req.query) + } catch (e) { + if (e instanceof ValidationError) { + return res.status(400).json(e) + } + console.error(`Unknown error during validation: ${e}`) + return res.status(500).json({ error: 'Unknown error during validation' }) + } + + const { limit, before } = params + + try { + const contracts = await listAllContracts(limit, before) + // Serve from Vercel cache, then update. see https://vercel.com/docs/concepts/functions/edge-caching + res.setHeader('Cache-Control', 's-maxage=1, stale-while-revalidate') + res.status(200).json(contracts.map(toLiteMarket)) + } catch (e) { + res.status(400).json({ + error: + 'Failed to fetch markets (did you pass an invalid ID as the before parameter?)', + }) + return + } +} diff --git a/web/pages/api/v0/me.ts b/web/pages/api/v0/me.ts new file mode 100644 index 00000000..7ee3cc3f --- /dev/null +++ b/web/pages/api/v0/me.ts @@ -0,0 +1,16 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { fetchBackend, forwardResponse } from 'web/lib/api/proxy' +import { LiteUser, ApiError } from './_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + try { + const backendRes = await fetchBackend(req, 'getcurrentuser') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ error: 'Error communicating with backend.' }) + } +} diff --git a/web/pages/api/v0/slug/[slug].ts b/web/pages/api/v0/slug/[slug].ts new file mode 100644 index 00000000..655b63a2 --- /dev/null +++ b/web/pages/api/v0/slug/[slug].ts @@ -0,0 +1,29 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { listAllBets } from 'web/lib/firebase/bets' +import { listAllComments } from 'web/lib/firebase/comments' +import { getContractFromSlug } from 'web/lib/firebase/contracts' +import { FullMarket, ApiError, toFullMarket } from '../_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { slug } = req.query + + const contract = await getContractFromSlug(slug as string) + + if (!contract) { + res.status(404).json({ error: 'Contract not found' }) + return + } + + const [bets, comments] = await Promise.all([ + listAllBets(contract.id), + listAllComments(contract.id), + ]) + + res.setHeader('Cache-Control', 'max-age=0') + return res.status(200).json(toFullMarket(contract, comments, bets)) +} diff --git a/web/pages/api/v0/user/[username]/bets/index.ts b/web/pages/api/v0/user/[username]/bets/index.ts new file mode 100644 index 00000000..464af52c --- /dev/null +++ b/web/pages/api/v0/user/[username]/bets/index.ts @@ -0,0 +1,25 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { Bet, getUserBets } from 'web/lib/firebase/bets' +import { getUserByUsername } from 'web/lib/firebase/users' +import { ApiError } from '../../../_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { username } = req.query + + const user = await getUserByUsername(username as string) + + if (!user) { + res.status(404).json({ error: 'User not found' }) + return + } + + const bets = await getUserBets(user.id, { includeRedemptions: false }) + + res.setHeader('Cache-Control', 'max-age=0') + return res.status(200).json(bets) +} diff --git a/web/pages/api/v0/user/[username]/index.ts b/web/pages/api/v0/user/[username]/index.ts new file mode 100644 index 00000000..58daffcd --- /dev/null +++ b/web/pages/api/v0/user/[username]/index.ts @@ -0,0 +1,19 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { getUserByUsername } from 'web/lib/firebase/users' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { LiteUser, ApiError, toLiteUser } from '../../_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { username } = req.query + const user = await getUserByUsername(username as string) + if (!user) { + res.status(404).json({ error: 'User not found' }) + return + } + res.setHeader('Cache-Control', 'no-cache') + return res.status(200).json(toLiteUser(user)) +} diff --git a/web/pages/api/v0/user/by-id/[id].ts b/web/pages/api/v0/user/by-id/[id].ts new file mode 100644 index 00000000..6ed67d1c --- /dev/null +++ b/web/pages/api/v0/user/by-id/[id].ts @@ -0,0 +1,19 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { getUser } from 'web/lib/firebase/users' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { LiteUser, ApiError, toLiteUser } from '../../_types' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const { id } = req.query + const user = await getUser(id as string) + if (!user) { + res.status(404).json({ error: 'User not found' }) + return + } + res.setHeader('Cache-Control', 'no-cache') + return res.status(200).json(toLiteUser(user)) +} diff --git a/web/pages/api/v0/users.ts b/web/pages/api/v0/users.ts new file mode 100644 index 00000000..8c62b601 --- /dev/null +++ b/web/pages/api/v0/users.ts @@ -0,0 +1,17 @@ +// Next.js API route support: https://vercel.com/docs/concepts/functions/serverless-functions +import type { NextApiRequest, NextApiResponse } from 'next' +import { listAllUsers } from 'web/lib/firebase/users' +import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors' +import { toLiteUser } from './_types' + +type Data = any[] + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + await applyCorsHeaders(req, res, CORS_UNRESTRICTED) + const users = await listAllUsers() + res.setHeader('Cache-Control', 'max-age=0') + res.status(200).json(users.map(toLiteUser)) +}