diff --git a/nuxt.config.ts b/nuxt.config.ts index d091838..a7cd6c5 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -73,10 +73,10 @@ export default defineNuxtConfig({ icon: { clientBundle: { - scan: true, + scan: true }, provider: 'none', - serverBundle: "local", + serverBundle: 'local' }, components: [ diff --git a/server/api/account/checkout.post.ts b/server/api/account/checkout.post.ts index a22c56e..9dd28d2 100644 --- a/server/api/account/checkout.post.ts +++ b/server/api/account/checkout.post.ts @@ -1,15 +1,18 @@ -import { usePapr } from "~~/server/utils/papr"; -import { ApiAccountCheckoutLink, CheckoutSchema } from "~~/shared/api-types"; +import { usePapr } from '~~/server/utils/papr'; +import { CheckoutSchema } from '~~/shared/api-types'; +import type { ApiAccountCheckoutLink } from '~~/shared/api-types'; export default defineEventHandler(async (event): Promise => { const auth = enforceLoggedIn(event); const papr = await usePapr(event); const stripe = useStripe(event); const config = useRuntimeConfig(event); - if (!stripe || !papr) throw createError({ - status: 400, - message: 'Stripe integration not configured', - }) + if (!stripe || !papr) { + throw createError({ + status: 400, + message: 'Stripe integration not configured' + }); + } const body = await readZodBody(event, CheckoutSchema); const { data: searchResults } = await stripe.customers.search({ @@ -29,15 +32,15 @@ export default defineEventHandler(async (event): Promise if (auth.accessLevel >= 2) { throw createError({ status: 400, - message: 'Staff members do not need to purchase tiers', - }) + message: 'Staff members do not need to purchase tiers' + }); } await papr.Pnid.updateOne({ pid: auth.pid }, { $set: { 'connections.stripe.customer_id': customer.id, 'connections.stripe.latest_webhook_timestamp': 0 } - }) + }); const priceId = body.priceId; const session = await stripe.checkout.sessions.create({ @@ -50,11 +53,13 @@ export default defineEventHandler(async (event): Promise customer: customer.id, mode: 'subscription', success_url: new URL('/account?upgrade_success=true', config.public.baseUrl).toString(), - cancel_url: new URL('/account?upgrade_success=false', config.public.baseUrl).toString(), + cancel_url: new URL('/account?upgrade_success=false', config.public.baseUrl).toString() }); - if (!session.url) throw new Error("Failed to create session"); + if (!session.url) { + throw new Error('Failed to create session'); + } return { - url: session.url, - } + url: session.url + }; }); diff --git a/server/api/account/discord-link.get.ts b/server/api/account/discord-link.get.ts index ea44a84..0b303a4 100644 --- a/server/api/account/discord-link.get.ts +++ b/server/api/account/discord-link.get.ts @@ -1,16 +1,18 @@ -import { useDiscord } from "~~/server/utils/discord"; -import { ApiAccountDiscordLink } from "~~/shared/api-types"; +import { useDiscord } from '~~/server/utils/discord'; +import type { ApiAccountDiscordLink } from '~~/shared/api-types'; export default defineEventHandler(async (event): Promise => { enforceLoggedIn(event); const discord = useDiscord(event); - if (!discord) throw createError({ - status: 400, - message: 'Discord integration not configured', - }) + if (!discord) { + throw createError({ + status: 400, + message: 'Discord integration not configured' + }); + } const redirectUrl = discord.makeCallbackUrl(); - const url = new URL("https://discord.com/oauth2/authorize"); + const url = new URL('https://discord.com/oauth2/authorize'); url.searchParams.set('response_type', 'code'); url.searchParams.set('client_id', 'code'); url.searchParams.set('scope', 'identify'); @@ -18,6 +20,6 @@ export default defineEventHandler(async (event): Promise url.searchParams.set('prompt', 'scope'); url.searchParams.set('integration_type', '1'); return { - url: url.toString(), - } + url: url.toString() + }; }); diff --git a/server/api/account/discord-unlink.post.ts b/server/api/account/discord-unlink.post.ts index 03e40df..10edd3b 100644 --- a/server/api/account/discord-unlink.post.ts +++ b/server/api/account/discord-unlink.post.ts @@ -1,18 +1,20 @@ -import { removeDiscordMemberSupporterRole, removeDiscordMemberTesterRole } from "~~/server/utils/discord"; +import { removeDiscordMemberSupporterRole, removeDiscordMemberTesterRole } from '~~/server/utils/discord'; export default defineEventHandler(async (event): Promise => { const auth = enforceLoggedIn(event); const discord = useDiscord(event); - if (!discord) throw createError({ - status: 400, - message: 'Discord integration not configured', - }) + if (!discord) { + throw createError({ + status: 400, + message: 'Discord integration not configured' + }); + } const grpc = useApiGrpcWithToken(event, auth.token); const oldUserData = await grpc.getUserData({}); const oldDiscordId = oldUserData.connections?.discord?.id; await grpc.setDiscordConnectionData({ - id: '', + id: '' }); const priceId = oldUserData.connections?.stripe?.priceId; diff --git a/server/api/account/tiers.get.ts b/server/api/account/tiers.get.ts index b93a8dc..b849334 100644 --- a/server/api/account/tiers.get.ts +++ b/server/api/account/tiers.get.ts @@ -1,24 +1,30 @@ -import { ApiAccountTiers, TierItem } from "~~/shared/api-types"; +import type { ApiAccountTiers, TierItem } from '~~/shared/api-types'; export default defineEventHandler(async (event): Promise => { enforceLoggedIn(event); const stripe = useStripe(event); - if (!stripe) throw createError({ - status: 400, - message: 'Stripe integration not configured', - }) + if (!stripe) { + throw createError({ + status: 400, + message: 'Stripe integration not configured' + }); + } const prices = await stripe.prices.list().autoPagingToArray({ limit: 10 }); const products = await stripe.products.list().autoPagingToArray({ limit: 10 }); const tiers: TierItem[] = []; - for (let product of products) { - if (!product.active) continue; + for (const product of products) { + if (!product.active) { + continue; + } const price = prices.find(price => price.id === product.default_price); - if (!price) continue; + if (!price) { + continue; + } - const tierLevel = Number(product.metadata.tier_level ?? "0"); + const tierLevel = Number(product.metadata.tier_level ?? '0'); const hasDiscordReadPerk = product.metadata.discord_read === 'true'; const hasBetaAccessPerk = product.metadata.beta === 'true'; @@ -31,12 +37,12 @@ export default defineEventHandler(async (event): Promise => { description: product.description, perks: { discordRead: hasDiscordReadPerk, - beta: hasBetaAccessPerk, - }, - }) + beta: hasBetaAccessPerk + } + }); } return { - tiers, - } + tiers + }; }); diff --git a/server/api/account/update.patch.ts b/server/api/account/update.patch.ts index 107cd4e..70c365f 100644 --- a/server/api/account/update.patch.ts +++ b/server/api/account/update.patch.ts @@ -1,4 +1,4 @@ -import { AccountUpdateSchema } from "~~/shared/api-types"; +import { AccountUpdateSchema } from '~~/shared/api-types'; export default defineEventHandler(async (event): Promise => { const body = await readZodBody(event, AccountUpdateSchema); @@ -8,11 +8,11 @@ export default defineEventHandler(async (event): Promise => { // There's no equivalent GRPC endpoint to use, so we're using the HTTP api await apiFetch('/v1/user', { headers: { - 'Authorization': `Bearer ${auth.token}`, + Authorization: `Bearer ${auth.token}` }, body: { mii: body.mii, environment: body.environment } - }) + }); }); diff --git a/server/api/auth/forgot-password.post.ts b/server/api/auth/forgot-password.post.ts index 797e7b0..a60f9fe 100644 --- a/server/api/auth/forgot-password.post.ts +++ b/server/api/auth/forgot-password.post.ts @@ -1,17 +1,19 @@ -import { hcaptchaVerify } from "~~/server/utils/hcaptcha"; -import { ForgotPasswordSchema } from "~~/shared/api-types"; +import { hcaptchaVerify } from '~~/server/utils/hcaptcha'; +import { ForgotPasswordSchema } from '~~/shared/api-types'; export default defineEventHandler(async (event): Promise => { const body = await readZodBody(event, ForgotPasswordSchema); const grpc = useApiGrpc(event); const captchaResult = await hcaptchaVerify(event, body.captchaResponse); - if (!captchaResult) throw createError({ - status: 400, - message: 'Invalid captcha', - }); + if (!captchaResult) { + throw createError({ + status: 400, + message: 'Invalid captcha' + }); + } await grpc.forgotPassword({ - emailAddressOrUsername: body.emailOrPassword, - }) + emailAddressOrUsername: body.emailOrPassword + }); }); diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts index 0bde814..79cbbab 100644 --- a/server/api/auth/login.post.ts +++ b/server/api/auth/login.post.ts @@ -1,6 +1,6 @@ -import { ServerError } from "nice-grpc"; -import { ApiAuthLogin, LoginSchema } from "#shared/api-types" -import { useLegacyApiGrpc } from "~~/server/utils/useGrpc"; +import { ServerError } from 'nice-grpc'; +import { LoginSchema } from '#shared/api-types'; +import type { ApiAuthLogin } from '#shared/api-types'; export default defineEventHandler(async (event): Promise => { const body = await readZodBody(event, LoginSchema); diff --git a/server/api/auth/me.get.ts b/server/api/auth/me.get.ts index c7573de..c9a71cc 100644 --- a/server/api/auth/me.get.ts +++ b/server/api/auth/me.get.ts @@ -1,4 +1,4 @@ -import type { GetApiAuthMe } from "#shared/api-types" +import type { GetApiAuthMe } from '#shared/api-types'; export default defineEventHandler(async (event): Promise => { const auth = enforceLoggedIn(event); @@ -8,9 +8,11 @@ export default defineEventHandler(async (event): Promise => { return { pid: data.pid, username: data.username, - mii: data.mii ? { - imageUrl: `https://r2-cdn.pretendo.cc/mii/${data.pid}/normal_face.png`, - name: data.mii.name, - } : null, + mii: data.mii + ? { + imageUrl: `https://r2-cdn.pretendo.cc/mii/${data.pid}/normal_face.png`, + name: data.mii.name + } + : null }; }); diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts index ce5d065..0575309 100644 --- a/server/api/auth/register.post.ts +++ b/server/api/auth/register.post.ts @@ -1,9 +1,11 @@ -import { ApiAuthLogin, RegisterSchema } from "#shared/api-types" +import { RegisterSchema } from '#shared/api-types'; +import type { ApiAuthLogin } from '#shared/api-types'; export default defineEventHandler(async (event): Promise => { const body = await readZodBody(event, RegisterSchema); const grpc = useApiGrpc(event); + // eslint-disable-next-line no-useless-catch -- Temp before error handling is implemented try { // TODO Add ip // TODO Add birthday @@ -13,7 +15,7 @@ export default defineEventHandler(async (event): Promise => { captchaResponse: body.captchaResponse, username: body.username, password: body.password, - passwordConfirm: body.password, + passwordConfirm: body.password }); return { @@ -21,6 +23,7 @@ export default defineEventHandler(async (event): Promise => { refreshToken: res.refreshToken }; } catch (error: unknown) { + // TODO handle errors throw error; } }); diff --git a/server/api/auth/reset-password.post.ts b/server/api/auth/reset-password.post.ts index f35504e..ea38587 100644 --- a/server/api/auth/reset-password.post.ts +++ b/server/api/auth/reset-password.post.ts @@ -1,4 +1,4 @@ -import { ResetPasswordSchema } from "~~/shared/api-types"; +import { ResetPasswordSchema } from '~~/shared/api-types'; export default defineEventHandler(async (event): Promise => { const body = await readZodBody(event, ResetPasswordSchema); @@ -7,6 +7,6 @@ export default defineEventHandler(async (event): Promise => { await grpc.resetPassword({ password: body.password, passwordConfirm: body.passwordConfirm, - token: body.resetToken, - }) + token: body.resetToken + }); }); diff --git a/server/api/progress.get.ts b/server/api/progress.get.ts index f1aea6a..1e0b48c 100644 --- a/server/api/progress.get.ts +++ b/server/api/progress.get.ts @@ -1,6 +1,6 @@ -import type { GetProgress, ProgressItem } from "#shared/api-types" -import { getGithubProjects } from "../utils/getGithubProgress"; -import { getStripeDonations } from "../utils/getStripeDonations"; +import { getGithubProjects } from '../utils/getGithubProgress'; +import { getStripeDonations } from '../utils/getStripeDonations'; +import type { GetProgress, ProgressItem } from '#shared/api-types'; const donationGoalCents = 3000 * 100; @@ -10,7 +10,7 @@ export default defineEventHandler(async (event): Promise => { const donationData = await getStripeDonations(stripe); const { projects } = await getGithubProjects(octokit); - const items: ProgressItem[] = projects.map(v => { + const items: ProgressItem[] = projects.map((v) => { const totalTasks = v.tasks.length; const completedTasks = v.tasks.filter(v => v.status === 'completed').length; const percentage = Math.floor(completedTasks / totalTasks * 100); @@ -21,9 +21,9 @@ export default defineEventHandler(async (event): Promise => { completion: percentage, tasks: v.tasks.map(task => ({ status: task.status, - title: task.title, + title: task.title })) - } + }; }); const summedCompletion = items.reduce((a, v) => a + v.completion, 0); const completionPercentage = Math.floor(summedCompletion / items.length * 100); @@ -32,9 +32,9 @@ export default defineEventHandler(async (event): Promise => { completion: completionPercentage, donations: { currentCents: donationData.totalDonationsCents, - goalCents: donationGoalCents, + goalCents: donationGoalCents }, - items: projects.map(v => { + items: projects.map((v) => { const totalTasks = v.tasks.length; const completedTasks = v.tasks.filter(v => v.status === 'completed').length; const percentage = Math.floor(completedTasks / totalTasks * 100); @@ -45,9 +45,9 @@ export default defineEventHandler(async (event): Promise => { completion: percentage, tasks: v.tasks.map(task => ({ status: task.status, - title: task.title, + title: task.title })) - } + }; }) }; }); diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts index e5ca46c..242b84f 100644 --- a/server/middleware/auth.ts +++ b/server/middleware/auth.ts @@ -1,6 +1,6 @@ export default defineEventHandler(async (event) => { const authHeader = getRequestHeader(event, 'authorization'); - setAuthContext(event, null) + setAuthContext(event, null); if (authHeader) { try { const [type, token] = authHeader.split(' ', 2); @@ -18,8 +18,8 @@ export default defineEventHandler(async (event) => { username: userData.username, token: token, accessLevel: userData.accessLevel, - email: userData.emailAddress, - }) + email: userData.emailAddress + }); } catch (err) { console.error('Failed to request user data: ', err); return; // Continue like nothing happened, further steps will validate if authed diff --git a/server/routes/account/connect/discord.get.ts b/server/routes/account/connect/discord.get.ts index af36ffc..d7fb600 100644 --- a/server/routes/account/connect/discord.get.ts +++ b/server/routes/account/connect/discord.get.ts @@ -1,18 +1,18 @@ -import { assignDiscordMemberSupporterRole, assignDiscordMemberTesterRole } from "~~/server/utils/discord"; +import { assignDiscordMemberSupporterRole, assignDiscordMemberTesterRole } from '~~/server/utils/discord'; type DiscordTokenResponse = { - "access_token": string, - "token_type": string, - "expires_in": number, - "refresh_token": string, - "scope": string -} + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + scope: string; +}; type DiscordUserResponse = { - "user"?: { - id: string, - } -} + user?: { + id: string; + }; +}; // Discord oauth callback export default defineEventHandler(async (event) => { @@ -22,7 +22,7 @@ export default defineEventHandler(async (event) => { } const discordFetch = $fetch.create({ - baseURL: discord.baseUrl, + baseURL: discord.baseUrl }); const accessTokenCookie = getCookie(event, 'access_token'); const query = getQuery(event); @@ -34,14 +34,14 @@ export default defineEventHandler(async (event) => { const tokens = await discordFetch('/oauth2/token', { body: new URLSearchParams({ - 'grant_type': 'authorization_code', - 'code': authCode, - 'redirect_uri': discord.makeCallbackUrl(), + grant_type: 'authorization_code', + code: authCode, + redirect_uri: discord.makeCallbackUrl() }) }); const authInfo = await discordFetch('/oauth2/@me', { headers: { - 'Authorization': `Bearer ${tokens.access_token}` + Authorization: `Bearer ${tokens.access_token}` } }); if (!authInfo.user) { diff --git a/server/routes/account/sso/discourse.get.ts b/server/routes/account/sso/discourse.get.ts index 640eed5..159572a 100644 --- a/server/routes/account/sso/discourse.get.ts +++ b/server/routes/account/sso/discourse.get.ts @@ -1,5 +1,5 @@ -import { GetUserDataResponse } from "@pretendonetwork/grpc/api/get_user_data_rpc"; -import { createHmac } from "node:crypto"; +import { createHmac } from 'node:crypto'; +import type { GetUserDataResponse } from '@pretendonetwork/grpc/api/get_user_data_rpc'; function getDicourseSignature(secret: string, payload: string) { return createHmac('sha256', secret).update(payload).digest('hex'); @@ -42,7 +42,7 @@ export default defineEventHandler(async (event) => { const redirect = `/sso/discourse?${redirectUrlParams}`; const urlParams = new URLSearchParams(); - urlParams.append('redirect', redirect) + urlParams.append('redirect', redirect); return sendRedirect(event, `/login?${urlParams}`); // Not logged in, redirect to login } diff --git a/server/routes/account/stripe/webhook.post.ts b/server/routes/account/stripe/webhook.post.ts index 97c18a7..013ac68 100644 --- a/server/routes/account/stripe/webhook.post.ts +++ b/server/routes/account/stripe/webhook.post.ts @@ -1,32 +1,40 @@ -import { Stripe } from "stripe"; -import { handleStripeEvent } from "~~/server/utils/handleStripeWebhook"; +import { handleStripeEvent } from '~~/server/utils/handleStripeWebhook'; +import type { Stripe } from 'stripe'; -export default defineEventHandler(async (event): Promise<{ success: boolean, message?: string }> => { +export default defineEventHandler(async (event): Promise<{ success: boolean; message?: string }> => { const config = useRuntimeConfig(event); const stripe = useStripe(event); - if (!stripe || !config.stripeWebhookSecret) throw new Error('Stripe not configured on this instance'); + if (!stripe || !config.stripeWebhookSecret) { + throw new Error('Stripe not configured on this instance'); + } let webhookEvent: Stripe.Event; try { const signatureHeader = getHeader(event, 'stripe-signature'); - if (!signatureHeader) throw new Error('No signature header on webhook event'); + if (!signatureHeader) { + throw new Error('No signature header on webhook event'); + } const rawBody = await readRawBody(event) ?? ''; - if (!rawBody) throw new Error('No body on webhook event'); + if (!rawBody) { + throw new Error('No body on webhook event'); + } webhookEvent = stripe.webhooks.constructEvent(rawBody, signatureHeader, config.stripeWebhookSecret); } catch (error) { console.error(error); setResponseStatus(event, 400); return { success: false, - message: 'Invalid webhook', - } + message: 'Invalid webhook' + }; } const notificationEmails: string[] = []; - if (config.stripeNotificationEmail) notificationEmails.push(config.stripeNotificationEmail); + if (config.stripeNotificationEmail) { + notificationEmails.push(config.stripeNotificationEmail); + } await handleStripeEvent(event, stripe, webhookEvent, notificationEmails); return { - success: true, - } + success: true + }; }); diff --git a/server/utils/discord.ts b/server/utils/discord.ts index ec211dc..061adad 100644 --- a/server/utils/discord.ts +++ b/server/utils/discord.ts @@ -1,17 +1,17 @@ import { REST } from '@discordjs/rest'; import { Routes } from 'discord-api-types/v10'; -import type { H3Event } from 'h3' +import type { H3Event } from 'h3'; -type DiscordIds = { guildId: string, supporterRoleId: string | null, testerRoleId: string | null } +type DiscordIds = { guildId: string; supporterRoleId: string | null; testerRoleId: string | null }; type DiscordInstance = { - rest: REST, - clientId: string, - clientSecret: string, - baseUrl: string, - ids: DiscordIds, - makeCallbackUrl: () => string, -} + rest: REST; + clientId: string; + clientSecret: string; + baseUrl: string; + ids: DiscordIds; + makeCallbackUrl: () => string; +}; let discordInstance: DiscordInstance | null = null; @@ -21,18 +21,18 @@ export function useDiscord(event: H3Event): DiscordInstance | null { if (config.discordBotToken && config.discordGuildId && config.discordClientId && config.discordClientSecret) { discordInstance = { rest: new REST({ version: '10' }).setToken(config.discordBotToken), - baseUrl: "https://discord.com/api/v10", + baseUrl: 'https://discord.com/api/v10', clientId: config.discordClientId, clientSecret: config.discordClientSecret, ids: { guildId: config.discordGuildId, testerRoleId: config.discordTesterRoleId ? config.discordTesterRoleId : null, - supporterRoleId: config.discordSupporterRoleId ? config.discordSupporterRoleId : null, + supporterRoleId: config.discordSupporterRoleId ? config.discordSupporterRoleId : null }, makeCallbackUrl() { - return new URL('/account/connect/discord', config.public.baseUrl).toString() - }, - } + return new URL('/account/connect/discord', config.public.baseUrl).toString(); + } + }; } } diff --git a/server/utils/enforceAuth.ts b/server/utils/enforceAuth.ts index 3e69dcd..1d1c02e 100644 --- a/server/utils/enforceAuth.ts +++ b/server/utils/enforceAuth.ts @@ -1,4 +1,4 @@ -import type { H3Event } from 'h3' +import type { H3Event } from 'h3'; export type AuthContext = { pid: number; diff --git a/server/utils/getGithubProgress.ts b/server/utils/getGithubProgress.ts index 45cdfe0..b758823 100644 --- a/server/utils/getGithubProgress.ts +++ b/server/utils/getGithubProgress.ts @@ -1,4 +1,4 @@ -import { Octokit } from "octokit"; +import type { Octokit } from 'octokit'; export type GithubProjectTaskStatus = 'completed' | 'inprogress' | 'notstarted'; @@ -7,17 +7,17 @@ export type GithubProject = { url: string; tasks: Array<{ title: string; - status: GithubProjectTaskStatus, - }> -} + status: GithubProjectTaskStatus; + }>; +}; export type GithubProjectResponse = { projects: GithubProject[]; -} +}; -const orgName = "PretendoNetwork"; +const orgName = 'PretendoNetwork'; const cacheMaxAgeMs = 60 * 60 * 1000; // 1 hour -let cache: { response: GithubProjectResponse, createdAt: Date } | null = null; +let cache: { response: GithubProjectResponse; createdAt: Date } | null = null; const getProjectsV2GQL = ` query getProjectsV2($orgName: String!, $cursor: String) { @@ -79,7 +79,7 @@ query getProjectsV2Fields($id: ID!, $cursor: String) { `; async function getGitHubProjectsV2(octokit: Octokit) { - const projects: Array<{ id: string, title: string, url: string | null }> = []; + const projects: Array<{ id: string; title: string; url: string | null }> = []; const data = await octokit.graphql.paginate(getProjectsV2GQL, { orgName: orgName @@ -89,7 +89,7 @@ async function getGitHubProjectsV2(octokit: Octokit) { projects.push({ id: node.id, title: node.title, - url: node.repositories.nodes[0]?.url ?? null, + url: node.repositories.nodes[0]?.url ?? null }); } @@ -97,7 +97,7 @@ async function getGitHubProjectsV2(octokit: Octokit) { } async function getGitHubProjectsV2Fields(octokit: Octokit, id: string) { - const output: Array<{ title: string, column: string }> = []; + const output: Array<{ title: string; column: string }> = []; const data: any = await octokit.graphql.paginate(getProjectsV2FieldsGQL, { id: id @@ -125,7 +125,7 @@ async function getGithubProjectsData(octokit: Octokit): Promise = { done: 'completed', in_progress: 'inprogress', - todo: 'notstarted', - } + todo: 'notstarted' + }; for (const field of fields) { const normalizedStatus = field.column.toLowerCase().replace(' ', '_'); const status = fieldMap[normalizedStatus]; - if (!status) continue; + if (!status) { + continue; + } projectOutput.tasks.push({ status, - title: field.title, - }) + title: field.title + }); } output.push(projectOutput); } return { - projects: output, + projects: output }; } @@ -159,8 +161,8 @@ export async function getGithubProjects(octokit: Octokit | null, ignoreCache = f // No github credentials, assume there are no projects if (!octokit) { return { - projects: [], - } + projects: [] + }; } cache = { diff --git a/server/utils/getStripeDonations.ts b/server/utils/getStripeDonations.ts index 5537e10..7393312 100644 --- a/server/utils/getStripeDonations.ts +++ b/server/utils/getStripeDonations.ts @@ -1,25 +1,27 @@ -import Stripe from "stripe"; +import type { Stripe } from 'stripe'; export type StripeDonationResponse = { donatorCount: number; totalDonationsCents: number; -} +}; const cacheMaxAgeMs = 60 * 60 * 1000; // 1 hour -let cache: { response: StripeDonationResponse, createdAt: Date } | null = null; +let cache: { response: StripeDonationResponse; createdAt: Date } | null = null; async function getStripeDonationData(stripe: Stripe): Promise { const donationData: StripeDonationResponse = { donatorCount: 0, - totalDonationsCents: 0, + totalDonationsCents: 0 }; await stripe.subscriptions.list({ limit: 100, - status: 'active', + status: 'active' }).autoPagingEach((sub) => { const plan = sub.items.data[0]?.plan; - if (!plan) return; + if (!plan) { + return; + } donationData.donatorCount += 1; donationData.totalDonationsCents += plan?.amount ?? 0; }); @@ -27,15 +29,14 @@ async function getStripeDonationData(stripe: Stripe): Promise { if (!cache || new Date(cache.createdAt.getTime() + cacheMaxAgeMs) < new Date() || ignoreCache) { // No credentials, fill in blank data if (!stripe) { return { donatorCount: 0, - totalDonationsCents: 0, - } + totalDonationsCents: 0 + }; } cache = { diff --git a/server/utils/handleStripeWebhook.ts b/server/utils/handleStripeWebhook.ts index 67859b1..f0ce354 100644 --- a/server/utils/handleStripeWebhook.ts +++ b/server/utils/handleStripeWebhook.ts @@ -1,32 +1,35 @@ -import { Stripe } from "stripe"; -import { useMailer } from "./mailer"; -import { PnidDocument, usePapr } from "./papr"; -import { Transporter } from "nodemailer"; -import { PaprMatchKeysAndValues } from "papr"; +import { useMailer } from './mailer'; +import { usePapr } from './papr'; +import { assignDiscordMemberSupporterRole, assignDiscordMemberTesterRole, removeDiscordMemberSupporterRole, removeDiscordMemberTesterRole, useDiscord } from './discord'; +import type { Transporter } from 'nodemailer'; +import type { PaprMatchKeysAndValues } from 'papr'; +import type { Stripe } from 'stripe'; import type { H3Event } from 'h3'; -import { assignDiscordMemberSupporterRole, assignDiscordMemberTesterRole, removeDiscordMemberSupporterRole, removeDiscordMemberTesterRole, useDiscord } from "./discord"; +import type { PnidDocument } from './papr'; -async function sendEmailToCustomer(mailer: Transporter, customer: Stripe.Customer, ops: { pid: number, title: string, body: string }): Promise { +async function sendEmailToCustomer(mailer: Transporter, customer: Stripe.Customer, ops: { pid: number; title: string; body: string }): Promise { try { - if (!customer.email) throw new Error("Customer does not have an email"); + if (!customer.email) { + throw new Error('Customer does not have an email'); + } await mailer.sendMail({ to: customer.email, subject: ops.title, - text: ops.body, + text: ops.body }); } catch (error) { console.error(`Error sending email | ${customer.id}, ${ops.pid}, ${customer.email} |`, error); } } -async function sendToNotificationEmails(mailer: Transporter, notificationEmails: string[], ops: { title: string, body: string }): Promise { +async function sendToNotificationEmails(mailer: Transporter, notificationEmails: string[], ops: { title: string; body: string }): Promise { for (const email of notificationEmails) { // * Send notification emails for new sub try { await mailer.sendMail({ to: email, subject: `[Pretendo] - ${ops.title}`, - text: ops.body, + text: ops.body }); } catch (error) { console.error(`Error sending notification email | ${email} |`, error); @@ -49,7 +52,9 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook: if (webhook.type === 'customer.subscription.updated' || webhook.type === 'customer.subscription.deleted') { const subscription = webhook.data.object; const subscriptionItem = subscription.items.data[0]; - if (!subscriptionItem) throw new Error("No subscription item subscription"); + if (!subscriptionItem) { + throw new Error('No subscription item subscription'); + } const product = await stripe.products.retrieve(subscriptionItem.plan.product as string); const customer = await stripe.customers.retrieve(subscription.customer as string); @@ -69,7 +74,9 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook: const invoice = await stripe.invoices.retrieve(subscription.latest_invoice as string); const intent = invoice.payments?.data[0]?.payment.payment_intent; - if (!intent) throw new Error("No intent found") + if (!intent) { + throw new Error('No intent found'); + } await stripe.refunds.create({ payment_intent: intent as string }); @@ -81,7 +88,7 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook: pid: 0, title: 'Pretendo Network Subscription Failed - No Linked PNID', body: `Your recent subscription to Pretendo Network has failed.\nThis is due to no PNID PID being linked to the Stripe customer account used. The subscription has been canceled and refunded. Please contact Jon immediately.\nStripe Customer ID: ${customer.id}` - }) + }); } else { console.error(`Stripe user ${customer.id} has no PNID linked!`); } @@ -104,7 +111,9 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook: if (subscription.latest_invoice) { const invoice = await stripe.invoices.retrieve(subscription.latest_invoice as string); const intent = invoice.payments?.data[0]?.payment.payment_intent; - if (!intent) throw new Error("No intent found") + if (!intent) { + throw new Error('No intent found'); + } await stripe.refunds.create({ payment_intent: intent as string }); @@ -117,7 +126,7 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook: pid: 0, title: 'Pretendo Network Subscription Failed - PNID Not Found', body: `Your recent subscription to Pretendo Network has failed.\nThis is due to the provided PNID not being found. The subscription has been canceled and refunded. Please contact Jon immediately.\nStripe Customer ID: ${customer.id}\nPNID PID: ${pid}` - }) + }); } else { console.error(`PNID PID ${pid} does not exist! Found on Stripe user ${customer.id}!`); } @@ -235,7 +244,7 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook: pid, title: `Pretendo Network ${product.name} Subscription - Active`, body: `Thank you for purchasing the ${product.name} tier! We greatly value your support, thank you for helping keep Pretendo Network alive!\nIt may take a moment for your account dashboard to reflect these changes. Please wait a moment and refresh the dashboard to see them!` - }) + }); if (discord && discordId && product.metadata.discord_role_id) { await assignDiscordMemberSupporterRole(discord, discordId, product.metadata.discord_role_id).catch((error) => { @@ -245,14 +254,14 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook: await sendToNotificationEmails(mailer, notificationEmails, { title: `New ${product.name} subscription`, - body: `${pnid.username} just became a ${product.name} tier subscriber`, - }) + body: `${pnid.username} just became a ${product.name} tier subscriber` + }); } else if (subscription.status === 'canceled') { await sendEmailToCustomer(mailer, customer, { pid, title: `Pretendo Network ${product.name} Subscription - Canceled`, body: `Your subscription for the ${product.name} tier has been canceled. We thank for your previous support, and hope you still enjoy the network! ` - }) + }); if (discord && discordId && product.metadata.discord_role_id) { await removeDiscordMemberSupporterRole(discord, discordId, product.metadata.discord_role_id).catch((error) => { @@ -262,14 +271,14 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook: await sendToNotificationEmails(mailer, notificationEmails, { title: `Canceled ${product.name} subscription`, - body: `${pnid.username} just canceled their ${product.name} tier subscription`, - }) + body: `${pnid.username} just canceled their ${product.name} tier subscription` + }); } else if (subscription.status === 'unpaid') { await sendEmailToCustomer(mailer, customer, { pid, title: `Pretendo Network ${product.name} Subscription - Unpaid`, body: `Your subscription for the ${product.name} tier has been canceled due to non payment. We thank for your previous support, and hope you still enjoy the network! ` - }) + }); if (discord && discordId && product.metadata.discord_role_id) { await removeDiscordMemberSupporterRole(discord, discordId, product.metadata.discord_role_id).catch((error) => { @@ -279,14 +288,14 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook: await sendToNotificationEmails(mailer, notificationEmails, { title: `Removed ${product.name} subscription`, - body: `${pnid.username}'s ${product.name} tier subscription has been canceled due to non payment`, - }) + body: `${pnid.username}'s ${product.name} tier subscription has been canceled due to non payment` + }); } else { await sendEmailToCustomer(mailer, customer, { pid, title: `Pretendo Network ${product.name} Subscription - ${subscription.status}`, body: `Your subscription for the ${product.name} tier has changed status to ${subscription.status}. This is usually caused by payment failure. Your account has been reverted back to default until payment resumes. If you believe this to be an error, please reach out for support on our Discord server, and we thank you for your previous support!` - }) + }); if (discord && discordId && product.metadata.discord_role_id) { await removeDiscordMemberSupporterRole(discord, discordId, product.metadata.discord_role_id).catch((error) => { @@ -296,8 +305,8 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook: await sendToNotificationEmails(mailer, notificationEmails, { title: `Removed ${product.name} subscription`, - body: `${pnid.username}'s ${product.name} tier subscription status has been changed to ${subscription.status}`, - }) + body: `${pnid.username}'s ${product.name} tier subscription status has been changed to ${subscription.status}` + }); } } } diff --git a/server/utils/hcaptcha.ts b/server/utils/hcaptcha.ts index 39476b2..dda18a9 100644 --- a/server/utils/hcaptcha.ts +++ b/server/utils/hcaptcha.ts @@ -1,14 +1,22 @@ -import type { H3Event } from 'h3' -import hcaptcha from 'hcaptcha' +import hcaptcha from 'hcaptcha'; +import type { H3Event } from 'h3'; export async function hcaptchaVerify(event: H3Event, captchaResponse: string | null | undefined): Promise { const config = useRuntimeConfig(event); - if (!config.hcaptchaSiteKey) return true; // No captcha is configured, always valid - if (!config.hcaptchaSecretKey) throw new Error("Hcaptcha not configured correctly, missing secret key"); + if (!config.hcaptchaSiteKey) { + return true; + } // No captcha is configured, always valid + if (!config.hcaptchaSecretKey) { + throw new Error('Hcaptcha not configured correctly, missing secret key'); + } - if (!captchaResponse) return false; // No captcha filled in, invalid + if (!captchaResponse) { + return false; + } // No captcha filled in, invalid const captchaVerify = await hcaptcha.verify(config.hcaptchaSiteKey, captchaResponse, undefined, config.hcaptchaSiteKey); - if (!captchaVerify.success) return false; // Invalid captcha response + if (!captchaVerify.success) { + return false; + } // Invalid captcha response return true; } diff --git a/server/utils/httpApi.ts b/server/utils/httpApi.ts index c8a9ab3..41ed96d 100644 --- a/server/utils/httpApi.ts +++ b/server/utils/httpApi.ts @@ -1,6 +1,6 @@ export function useHttpApi(event: H3Event) { const config = useRuntimeConfig(event); return $fetch.create({ - baseURL: config.public.apiBase, + baseURL: config.public.apiBase }); } diff --git a/server/utils/mailer.ts b/server/utils/mailer.ts index ddb81bd..399eed2 100644 --- a/server/utils/mailer.ts +++ b/server/utils/mailer.ts @@ -1,5 +1,6 @@ -import { createTransport, Transporter } from "nodemailer"; -import type { H3Event } from 'h3' +import { createTransport } from 'nodemailer'; +import type { Transporter } from 'nodemailer'; +import type { H3Event } from 'h3'; let transport: Transporter | null = null; @@ -10,7 +11,7 @@ export function useMailer(event: H3Event): Transporter | null { transport = createTransport({ from: { address: config.smtpFromEmail, - name: config.smtpFromName ? config.smtpFromName : undefined, + name: config.smtpFromName ? config.smtpFromName : undefined }, host: config.smtpHost, port: config.smtpPort, @@ -18,7 +19,7 @@ export function useMailer(event: H3Event): Transporter | null { auth: { user: config.smtpUser ? config.smtpUser : undefined, pass: config.smtpPassword ? config.smtpPassword : undefined - }, + } }); } } diff --git a/server/utils/octokit.ts b/server/utils/octokit.ts index faa77fe..569a93e 100644 --- a/server/utils/octokit.ts +++ b/server/utils/octokit.ts @@ -1,5 +1,5 @@ -import { Octokit } from "octokit" -import type { H3Event } from 'h3' +import { Octokit } from 'octokit'; +import type { H3Event } from 'h3'; let octokit: Octokit | null = null; diff --git a/server/utils/papr.ts b/server/utils/papr.ts index 9e7fcf0..5cba33d 100644 --- a/server/utils/papr.ts +++ b/server/utils/papr.ts @@ -1,6 +1,6 @@ -import { MongoClient } from "mongodb"; -import Papr, { schema, types } from "papr" -import type { H3Event } from 'h3' +import { MongoClient } from 'mongodb'; +import Papr, { schema, types } from 'papr'; +import type { H3Event } from 'h3'; const papr = new Papr(); let conn: MongoClient | null = null; @@ -17,10 +17,10 @@ const pnidSchema = schema({ price_id: types.string(), tier_level: types.number(), tier_name: types.string(), - latest_webhook_timestamp: types.number(), + latest_webhook_timestamp: types.number() }), discord: types.object({ - id: types.string(), + id: types.string() }) }) }); @@ -29,7 +29,7 @@ export type PnidDocument = (typeof pnidSchema)[0]; const paprInstance = { papr, - Pnid, + Pnid } as const; export type PaprInstance = typeof paprInstance; diff --git a/server/utils/readZodBody.ts b/server/utils/readZodBody.ts index 3126680..52af6ab 100644 --- a/server/utils/readZodBody.ts +++ b/server/utils/readZodBody.ts @@ -1,5 +1,5 @@ import type { z, ZodType } from 'zod'; -import type { H3Event } from 'h3' +import type { H3Event } from 'h3'; export async function readZodBody(event: H3Event, schema: T): Promise> { const body = await readValidatedBody(event, schema.safeParse); diff --git a/server/utils/stripe.ts b/server/utils/stripe.ts index ea0792b..0aa1e35 100644 --- a/server/utils/stripe.ts +++ b/server/utils/stripe.ts @@ -1,5 +1,5 @@ -import { Stripe } from "stripe" -import type { H3Event } from 'h3' +import { Stripe } from 'stripe'; +import type { H3Event } from 'h3'; let stripe: Stripe | null = null; diff --git a/server/utils/useGrpc.ts b/server/utils/useGrpc.ts index ab2223a..3c64ec7 100644 --- a/server/utils/useGrpc.ts +++ b/server/utils/useGrpc.ts @@ -3,7 +3,7 @@ import { ApiServiceDefinition } from '@pretendonetwork/grpc/api/v2/api_service'; import { APIDefinition } from '@pretendonetwork/grpc/api/api_service'; import { AccountServiceDefinition } from '@pretendonetwork/grpc/account/v2/account_service'; import type { Channel, Client, CompatServiceDefinition } from 'nice-grpc'; -import type { H3Event } from 'h3' +import type { H3Event } from 'h3'; let grpc: { channel: Channel } | null = null; diff --git a/shared/api-types.ts b/shared/api-types.ts index 9634862..bb1934a 100644 --- a/shared/api-types.ts +++ b/shared/api-types.ts @@ -7,26 +7,26 @@ export type GetApiAuthMe = { imageUrl: string; name: string; } | null; -} +}; export type ProgressItem = { - title: string, - githubUrl?: string, - completion: number, + title: string; + githubUrl?: string; + completion: number; tasks: Array<{ - status: 'completed' | 'inprogress' | 'notstarted', - title: string, - }> -} + status: 'completed' | 'inprogress' | 'notstarted'; + title: string; + }>; +}; export type GetProgress = { donations: { - currentCents: number, - goalCents: number, - }, + currentCents: number; + goalCents: number; + }; completion: number; items: ProgressItem[]; -} +}; export type ApiAuthLogin = { accessToken: string; @@ -34,11 +34,11 @@ export type ApiAuthLogin = { }; export type ApiAccountDiscordLink = { - url: string + url: string; }; export type ApiAccountCheckoutLink = { - url: string + url: string; }; export type TierItem = { @@ -51,12 +51,12 @@ export type TierItem = { perks: { discordRead: boolean; beta: boolean; - } -} + }; +}; export type ApiAccountTiers = { tiers: TierItem[]; -} +}; export const LoginSchema = z.object({ username: z.string(), @@ -86,7 +86,7 @@ export type ApiAccountUpdateRequest = z.infer; export const ResetPasswordSchema = z.object({ password: z.string(), passwordConfirm: z.string(), - resetToken: z.string(), + resetToken: z.string() }); export type ApiAuthResetPasswordRequest = z.infer; @@ -97,6 +97,6 @@ export const ForgotPasswordSchema = z.object({ export type ApiAuthForgotPasswordRequest = z.infer; export const CheckoutSchema = z.object({ - priceId: z.string(), + priceId: z.string() }); export type ApiAccountCheckoutRequest = z.infer; diff --git a/src/components/Navbar/Navbar.vue b/src/components/Navbar/Navbar.vue index 9d43a9b..9c1bfc4 100644 --- a/src/components/Navbar/Navbar.vue +++ b/src/components/Navbar/Navbar.vue @@ -470,23 +470,32 @@ onMounted(() => { v-if="user" class="user-widget-wrapper logged-in" > -
+
-
+
diff --git a/src/middleware/1.auth.global.ts b/src/middleware/1.auth.global.ts index 27b06b3..6b46c92 100644 --- a/src/middleware/1.auth.global.ts +++ b/src/middleware/1.auth.global.ts @@ -1,8 +1,10 @@ -import type { GetApiAuthMe } from "#shared/api-types" +import type { GetApiAuthMe } from '#shared/api-types'; export default defineNuxtRouteMiddleware(async () => { const meStore = useMeStore(); - if (meStore.loaded) return; // Already loaded + if (meStore.loaded) { + return; + } // Already loaded const authStore = useAuthStore(); const token = authStore.getToken(); @@ -14,13 +16,13 @@ export default defineNuxtRouteMiddleware(async () => { try { const res = await $fetch('/api/auth/me', { headers: { - 'Authorization': `Bearer ${token}` + Authorization: `Bearer ${token}` } - }) + }); meStore.setMe({ pid: res.pid, username: res.username, - mii: res.mii, + mii: res.mii }); } catch { meStore.setMe(null); diff --git a/src/middleware/2.enforce.global.ts b/src/middleware/2.enforce.global.ts index 8ae34a8..7c64a1d 100644 --- a/src/middleware/2.enforce.global.ts +++ b/src/middleware/2.enforce.global.ts @@ -1,12 +1,16 @@ function notAllowed() { - return navigateTo("/"); + return navigateTo('/'); } export default defineNuxtRouteMiddleware(async (to) => { const meStore = useMeStore(); - if (!meStore.loaded) throw new Error("Mestore must be loaded before reaching this middleware"); + if (!meStore.loaded) { + throw new Error('Mestore must be loaded before reaching this middleware'); + } if (to.meta.needsAuth) { - if (!meStore.user) return notAllowed(); + if (!meStore.user) { + return notAllowed(); + } } }); diff --git a/src/pages/account/index.vue b/src/pages/account/index.vue index 51d52cd..d824173 100644 --- a/src/pages/account/index.vue +++ b/src/pages/account/index.vue @@ -1,15 +1,14 @@ - diff --git a/src/pages/account/register/index.vue b/src/pages/account/register/index.vue index defd5f7..5087321 100644 --- a/src/pages/account/register/index.vue +++ b/src/pages/account/register/index.vue @@ -92,8 +92,8 @@ async function registerSubmission() { type="password" autocomplete="new-password" required - passwordrules="minlength: 6; maxlength: 16; max-consecutive: 2; allowed: [-!-~];" - pattern="[-!-~]{6,16}" + passwordrules="minlength: 6; maxlength: 16; max-consecutive: 2; allowed: [-!-~];" + pattern="[-!-~]{6,16}" >
diff --git a/src/pages/index.vue b/src/pages/index.vue index 94aed92..097cb4d 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -140,10 +140,15 @@ function titleSuffixHandler(path: string) {

{{ $t("progress.title") }} ({{ progress.data.value?.completion ?? 0 }}%)

-
-

{{ project.title }} [{{ project.completion }}%]

-
-

No projects

+
+

{{ project.title }} [{{ project.completion }}%]

+
+

+ No projects +

diff --git a/src/pages/progress.vue b/src/pages/progress.vue index ba1db15..34df72b 100644 --- a/src/pages/progress.vue +++ b/src/pages/progress.vue @@ -1,19 +1,34 @@ diff --git a/src/plugins/types.d.ts b/src/plugins/types.d.ts index 11ed680..688acc5 100644 --- a/src/plugins/types.d.ts +++ b/src/plugins/types.d.ts @@ -1,4 +1,4 @@ -declare module "#app" { +declare module '#app' { interface PageMeta { needsAuth?: boolean; } diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 7102258..41faaac 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-self-assign -- Self assign is needed for refresh cookies */ import type { CookieOptions } from '#app'; type AuthState = { @@ -73,6 +74,6 @@ export function useAuthStore() { getToken, refresh, set, - logout, + logout }; } diff --git a/src/stores/me.ts b/src/stores/me.ts index d3ea54f..ffc9e30 100644 --- a/src/stores/me.ts +++ b/src/stores/me.ts @@ -2,9 +2,9 @@ export type Me = { pid: number; username: string; mii: { - imageUrl: string, - name: string, - } | null, + imageUrl: string; + name: string; + } | null; }; export const useMeStore = defineStore('me', () => {