mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-08-22 16:47:16 -05:00
chore: fix linting in entire project
This commit is contained in:
@@ -73,10 +73,10 @@ export default defineNuxtConfig({
|
||||
|
||||
icon: {
|
||||
clientBundle: {
|
||||
scan: true,
|
||||
scan: true
|
||||
},
|
||||
provider: 'none',
|
||||
serverBundle: "local",
|
||||
serverBundle: 'local'
|
||||
},
|
||||
|
||||
components: [
|
||||
|
||||
@@ -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<ApiAccountCheckoutLink> => {
|
||||
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<ApiAccountCheckoutLink>
|
||||
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<ApiAccountCheckoutLink>
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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<ApiAccountDiscordLink> => {
|
||||
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<ApiAccountDiscordLink>
|
||||
url.searchParams.set('prompt', 'scope');
|
||||
url.searchParams.set('integration_type', '1');
|
||||
return {
|
||||
url: url.toString(),
|
||||
}
|
||||
url: url.toString()
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { removeDiscordMemberSupporterRole, removeDiscordMemberTesterRole } from "~~/server/utils/discord";
|
||||
import { removeDiscordMemberSupporterRole, removeDiscordMemberTesterRole } from '~~/server/utils/discord';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<void> => {
|
||||
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;
|
||||
|
||||
@@ -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<ApiAccountTiers> => {
|
||||
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<ApiAccountTiers> => {
|
||||
description: product.description,
|
||||
perks: {
|
||||
discordRead: hasDiscordReadPerk,
|
||||
beta: hasBetaAccessPerk,
|
||||
},
|
||||
})
|
||||
beta: hasBetaAccessPerk
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tiers,
|
||||
}
|
||||
tiers
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccountUpdateSchema } from "~~/shared/api-types";
|
||||
import { AccountUpdateSchema } from '~~/shared/api-types';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<void> => {
|
||||
const body = await readZodBody(event, AccountUpdateSchema);
|
||||
@@ -8,11 +8,11 @@ export default defineEventHandler(async (event): Promise<void> => {
|
||||
// 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
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<void> => {
|
||||
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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<ApiAuthLogin> => {
|
||||
const body = await readZodBody(event, LoginSchema);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GetApiAuthMe } from "#shared/api-types"
|
||||
import type { GetApiAuthMe } from '#shared/api-types';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<GetApiAuthMe> => {
|
||||
const auth = enforceLoggedIn(event);
|
||||
@@ -8,9 +8,11 @@ export default defineEventHandler(async (event): Promise<GetApiAuthMe> => {
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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<ApiAuthLogin> => {
|
||||
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<ApiAuthLogin> => {
|
||||
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<ApiAuthLogin> => {
|
||||
refreshToken: res.refreshToken
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
// TODO handle errors
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ResetPasswordSchema } from "~~/shared/api-types";
|
||||
import { ResetPasswordSchema } from '~~/shared/api-types';
|
||||
|
||||
export default defineEventHandler(async (event): Promise<void> => {
|
||||
const body = await readZodBody(event, ResetPasswordSchema);
|
||||
@@ -7,6 +7,6 @@ export default defineEventHandler(async (event): Promise<void> => {
|
||||
await grpc.resetPassword({
|
||||
password: body.password,
|
||||
passwordConfirm: body.passwordConfirm,
|
||||
token: body.resetToken,
|
||||
})
|
||||
token: body.resetToken
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<GetProgress> => {
|
||||
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<GetProgress> => {
|
||||
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<GetProgress> => {
|
||||
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<GetProgress> => {
|
||||
completion: percentage,
|
||||
tasks: v.tasks.map(task => ({
|
||||
status: task.status,
|
||||
title: task.title,
|
||||
title: task.title
|
||||
}))
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<DiscordTokenResponse>('/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<DiscordUserResponse>('/oauth2/@me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${tokens.access_token}`
|
||||
Authorization: `Bearer ${tokens.access_token}`
|
||||
}
|
||||
});
|
||||
if (!authInfo.user) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { H3Event } from 'h3'
|
||||
import type { H3Event } from 'h3';
|
||||
|
||||
export type AuthContext = {
|
||||
pid: number;
|
||||
|
||||
@@ -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<GithubProjectRes
|
||||
const projectOutput: GithubProject = {
|
||||
title: project.title,
|
||||
url: project.url,
|
||||
tasks: [],
|
||||
tasks: []
|
||||
};
|
||||
|
||||
const fields = await getGitHubProjectsV2Fields(octokit, project.id);
|
||||
@@ -133,24 +133,26 @@ async function getGithubProjectsData(octokit: Octokit): Promise<GithubProjectRes
|
||||
const fieldMap: Record<string, GithubProjectTaskStatus> = {
|
||||
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 = {
|
||||
|
||||
@@ -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<StripeDonationResponse> {
|
||||
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<StripeDonationResp
|
||||
return donationData;
|
||||
}
|
||||
|
||||
|
||||
export async function getStripeDonations(stripe: Stripe | null, ignoreCache = false): Promise<StripeDonationResponse> {
|
||||
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 = {
|
||||
|
||||
@@ -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<void> {
|
||||
async function sendEmailToCustomer(mailer: Transporter, customer: Stripe.Customer, ops: { pid: number; title: string; body: string }): Promise<void> {
|
||||
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<void> {
|
||||
async function sendToNotificationEmails(mailer: Transporter, notificationEmails: string[], ops: { title: string; body: string }): Promise<void> {
|
||||
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}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export function useHttpApi(event: H3Event) {
|
||||
const config = useRuntimeConfig(event);
|
||||
return $fetch.create({
|
||||
baseURL: config.public.apiBase,
|
||||
baseURL: config.public.apiBase
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { z, ZodType } from 'zod';
|
||||
import type { H3Event } from 'h3'
|
||||
import type { H3Event } from 'h3';
|
||||
|
||||
export async function readZodBody<T extends ZodType>(event: H3Event, schema: T): Promise<z.infer<T>> {
|
||||
const body = await readValidatedBody(event, schema.safeParse);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<typeof AccountUpdateSchema>;
|
||||
export const ResetPasswordSchema = z.object({
|
||||
password: z.string(),
|
||||
passwordConfirm: z.string(),
|
||||
resetToken: z.string(),
|
||||
resetToken: z.string()
|
||||
});
|
||||
export type ApiAuthResetPasswordRequest = z.infer<typeof ResetPasswordSchema>;
|
||||
|
||||
@@ -97,6 +97,6 @@ export const ForgotPasswordSchema = z.object({
|
||||
export type ApiAuthForgotPasswordRequest = z.infer<typeof ForgotPasswordSchema>;
|
||||
|
||||
export const CheckoutSchema = z.object({
|
||||
priceId: z.string(),
|
||||
priceId: z.string()
|
||||
});
|
||||
export type ApiAccountCheckoutRequest = z.infer<typeof CheckoutSchema>;
|
||||
|
||||
@@ -470,23 +470,32 @@ onMounted(() => {
|
||||
v-if="user"
|
||||
class="user-widget-wrapper logged-in"
|
||||
>
|
||||
<div class="user-widget-toggle" @click="widgetOpen = true">
|
||||
<div
|
||||
class="user-widget-toggle"
|
||||
@click="widgetOpen = true"
|
||||
>
|
||||
<img
|
||||
:src="user.mii?.imageUrl ?? '#'"
|
||||
:alt="user.mii?.name ?? ''"
|
||||
:alt="user.mii?.name ?? ''"
|
||||
>
|
||||
</div>
|
||||
<div class="user-widget" :class="{
|
||||
'active': widgetOpen
|
||||
}">
|
||||
<div
|
||||
class="user-widget"
|
||||
:class="{
|
||||
'active': widgetOpen
|
||||
}"
|
||||
>
|
||||
<div class="user-avatar">
|
||||
<img
|
||||
:src="user.mii?.imageUrl ?? '#'"
|
||||
:src="user.mii?.imageUrl ?? '#'"
|
||||
:alt="user.mii?.name ?? ''"
|
||||
>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div v-if="user.mii" class="mii-name">
|
||||
<div
|
||||
v-if="user.mii"
|
||||
class="mii-name"
|
||||
>
|
||||
{{ user.mii.name }}
|
||||
</div>
|
||||
<div class="pnid">
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
<slot />
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<GetApiAuthMe>('/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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
needsAuth: true,
|
||||
})
|
||||
needsAuth: true
|
||||
});
|
||||
|
||||
const me = useMeStore();
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Hello {{ me.user?.username }}</h1>
|
||||
<h1>Hello {{ me.user?.username }}</h1>
|
||||
<p>TODO: account stub page</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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}"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -140,10 +140,15 @@ function titleSuffixHandler(path: string) {
|
||||
<h2 class="title">
|
||||
<a href="/progress">{{ $t("progress.title") }} ({{ progress.data.value?.completion ?? 0 }}%)</a>
|
||||
</h2>
|
||||
<div v-for="project of progress.data.value?.items ?? []" :key="project.title">
|
||||
<p>{{ project.title }} [{{ project.completion }}%]</p>
|
||||
</div>
|
||||
<p v-if="(progress.data.value?.items ?? []).length === 0">No projects</p>
|
||||
<div
|
||||
v-for="project of progress.data.value?.items ?? []"
|
||||
:key="project.title"
|
||||
>
|
||||
<p>{{ project.title }} [{{ project.completion }}%]</p>
|
||||
</div>
|
||||
<p v-if="(progress.data.value?.items ?? []).length === 0">
|
||||
No projects
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
const progress = await useFetch('/api/progress');
|
||||
const projects = computed(() => progress.data.value?.items ?? []);
|
||||
const donations = computed(() => progress.data.value?.donations)
|
||||
const donations = computed(() => progress.data.value?.donations);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p>TODO: progress stub page</p>
|
||||
<h1 v-if="donations">{{ donations.currentCents }}¢ / {{ donations.goalCents }}¢</h1>
|
||||
<div v-for="project of projects" :key="project.title">
|
||||
<h1>{{ project.title }} ({{ project.completion }}%)</h1>
|
||||
<a v-if="project.githubUrl" :href="project.githubUrl">Github</a>
|
||||
<p v-for="task of project.tasks" :key="task.title">[{{ task.status }}] {{ task.title }}</p>
|
||||
<hr />
|
||||
</div>
|
||||
<p v-if="projects.length === 0">No projects</p>
|
||||
<h1 v-if="donations">
|
||||
{{ donations.currentCents }}¢ / {{ donations.goalCents }}¢
|
||||
</h1>
|
||||
<div
|
||||
v-for="project of projects"
|
||||
:key="project.title"
|
||||
>
|
||||
<h1>{{ project.title }} ({{ project.completion }}%)</h1>
|
||||
<a
|
||||
v-if="project.githubUrl"
|
||||
:href="project.githubUrl"
|
||||
>Github</a>
|
||||
<p
|
||||
v-for="task of project.tasks"
|
||||
:key="task.title"
|
||||
>
|
||||
[{{ task.status }}] {{ task.title }}
|
||||
</p>
|
||||
<hr>
|
||||
</div>
|
||||
<p v-if="projects.length === 0">
|
||||
No projects
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
2
src/plugins/types.d.ts
vendored
2
src/plugins/types.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
declare module "#app" {
|
||||
declare module '#app' {
|
||||
interface PageMeta {
|
||||
needsAuth?: boolean;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user