mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-08-24 01:26:57 -05:00
feat: implement mongo + mailing
This commit is contained in:
@@ -35,6 +35,14 @@ export default defineNuxtConfig({
|
||||
stripeSecretKey: '',
|
||||
grpcHost: '',
|
||||
grpcApiKey: '',
|
||||
mongoConnectionString: '',
|
||||
smtpHost: '',
|
||||
smtpPort: 587,
|
||||
smtpUser: '',
|
||||
smtpPassword: '',
|
||||
smtpSecure: true,
|
||||
smtpFromEmail: '',
|
||||
smtpFromName: '',
|
||||
|
||||
public: {
|
||||
apiBase: 'https://api.pretendo.cc',
|
||||
|
||||
177
package-lock.json
generated
177
package-lock.json
generated
@@ -20,9 +20,12 @@
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"eslint": "^9.39.5",
|
||||
"feed": "^6.0.0",
|
||||
"mongodb": "^7.5.0",
|
||||
"nice-grpc": "^2.1.17",
|
||||
"nodemailer": "^9.0.5",
|
||||
"nuxt": "^4.5.2",
|
||||
"octokit": "^5.0.5",
|
||||
"papr": "^17.1.1",
|
||||
"stripe": "^22.4.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^5.2.0",
|
||||
@@ -32,6 +35,7 @@
|
||||
"@iconify-json/fa7-brands": "^1.2.4",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@pretendonetwork/eslint-config": "^0.1.4",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"sass-embedded": "^1.86.3"
|
||||
}
|
||||
@@ -2566,6 +2570,15 @@
|
||||
"rollup": "^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.4.13",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.13.tgz",
|
||||
"integrity": "sha512-E3Sv4eCYAlKYUTx8S3ioQcDUscOif+8zZ5OnW1IzJ+Tt+EO+ke8mn+Y3FX6N1H79picwbdOavVOb1jPi2EOyrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
|
||||
@@ -6308,6 +6321,16 @@
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz",
|
||||
"integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-path": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
|
||||
@@ -6326,6 +6349,21 @@
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/webidl-conversions": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/whatwg-url": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
|
||||
"integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.66.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz",
|
||||
@@ -8217,6 +8255,15 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-7.3.1.tgz",
|
||||
"integrity": "sha512-h/C0qe6857pQhcSJHLfsR1uYGj98Ge3wKAD3Ed9KqH3wcVh+BM4Jq4xISD7vs9OPuT07n+q3QQVjslJ286j6ag==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
@@ -14506,6 +14553,12 @@
|
||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/memory-pager": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@@ -15274,6 +15327,100 @@
|
||||
"integrity": "sha512-aF7yRQr/Q0O2/4pIXm6PZ5G+jAd7QS4Yu8m+WEeEHGnbo+7mE36CbLSDQiXYV8bVL3NfmdeqPJct0tUlnjVSnA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.5.0.tgz",
|
||||
"integrity": "sha512-5FnrEDLnvp6ycUOGLNLLU33BfCx2qmp2mJjGPDwKLruYsVzXVSK5fsGpoDXvsXJwBfBsD7ebMRdawbDxC2814g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.4.11",
|
||||
"bson": "^7.2.0",
|
||||
"mongodb-connection-string-url": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.806.0",
|
||||
"@mongodb-js/zstd": "^7.0.0",
|
||||
"gcp-metadata": "^7.0.1",
|
||||
"kerberos": "^7.0.0",
|
||||
"mongodb-client-encryption": "^7.2.0",
|
||||
"snappy": "^7.3.2",
|
||||
"socks": "^2.8.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-providers": {
|
||||
"optional": true
|
||||
},
|
||||
"@mongodb-js/zstd": {
|
||||
"optional": true
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb-client-encryption": {
|
||||
"optional": true
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
},
|
||||
"socks": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.2.tgz",
|
||||
"integrity": "sha512-ZoS07RoFqpKYQwAk59qmrx8+jJHNHU30UjlU96QktiGn1ltvDr+vCznLX5DiUBLEpMAHatHNWV1nM/74ul66kA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^13.0.0",
|
||||
"whatwg-url": "^14.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||
@@ -15619,6 +15766,15 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.5.tgz",
|
||||
"integrity": "sha512-wvjiKvjczmsN7U/8006JOdXubgBk2XFAbioDMbT+sM7cPs0QrhJTa6KBRX7P5REGGkDcLUz/EarWidb8G8C1jQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz",
|
||||
@@ -16827,6 +16983,18 @@
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/papr": {
|
||||
"version": "17.1.1",
|
||||
"resolved": "https://registry.npmjs.org/papr/-/papr-17.1.1.tgz",
|
||||
"integrity": "sha512-5WL99VUNcbUqR+FCZk554iJRoB4vB32mNHAlZEJuBsZuTjBIQA/uJszWuJ8cEstGUIN0Ad7gjlB1RkZ4aA2hOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mongodb": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -19487,6 +19655,15 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/sparse-bitfield": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/spdx-exceptions": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
|
||||
|
||||
@@ -24,9 +24,12 @@
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"eslint": "^9.39.5",
|
||||
"feed": "^6.0.0",
|
||||
"mongodb": "^7.5.0",
|
||||
"nice-grpc": "^2.1.17",
|
||||
"nodemailer": "^9.0.5",
|
||||
"nuxt": "^4.5.2",
|
||||
"octokit": "^5.0.5",
|
||||
"papr": "^17.1.1",
|
||||
"stripe": "^22.4.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^5.2.0",
|
||||
@@ -36,6 +39,7 @@
|
||||
"@iconify-json/fa7-brands": "^1.2.4",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@pretendonetwork/eslint-config": "^0.1.4",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"sass-embedded": "^1.86.3"
|
||||
}
|
||||
|
||||
@@ -21,13 +21,13 @@ export default defineEventHandler(async (event): Promise<ApiAuthLogin> => {
|
||||
if (error.details === 'INVALID_ARGUMENT: User not found') {
|
||||
throw createError({
|
||||
status: 400,
|
||||
statusText: 'User not found'
|
||||
message: 'User not found'
|
||||
});
|
||||
}
|
||||
if (error.details === 'INVALID_ARGUMENT: Password is incorrect') {
|
||||
throw createError({
|
||||
status: 400,
|
||||
statusText: 'Password was incorrect'
|
||||
message: 'Password was incorrect'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { H3Event } from 'h3'
|
||||
|
||||
export type AuthContext = {
|
||||
pid: number;
|
||||
};
|
||||
@@ -15,7 +17,7 @@ export function enforceLoggedIn(event: H3Event): AuthContext {
|
||||
if (!context) {
|
||||
throw createError({
|
||||
status: 401,
|
||||
statusText: 'This action requires authentication'
|
||||
message: 'This action requires authentication'
|
||||
});
|
||||
}
|
||||
return context;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Stripe } from "stripe";
|
||||
import { useMailer } from "./mailer";
|
||||
import { PnidDocument, usePapr } from "./papr";
|
||||
import { Transporter } from "nodemailer";
|
||||
import { PaprMatchKeysAndValues } from "papr";
|
||||
import type { H3Event } from 'h3';
|
||||
|
||||
async function sendEmailToCustomer(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");
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: ops.title,
|
||||
@@ -12,7 +18,7 @@ async function sendEmailToCustomer(customer: Stripe.Customer, ops: { pid: number
|
||||
}
|
||||
}
|
||||
|
||||
async function sendToNotificationEmails(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 {
|
||||
@@ -28,6 +34,14 @@ async function sendToNotificationEmails(notificationEmails: string[], ops: { tit
|
||||
}
|
||||
|
||||
export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook: Stripe.Event, notificationEmails: string[]) {
|
||||
const mailer = useMailer(event);
|
||||
const papr = await usePapr(event);
|
||||
|
||||
if (!mailer || !papr) {
|
||||
console.warn('Mailer/database not configured, ignoring stripe webhook');
|
||||
return;
|
||||
}
|
||||
|
||||
if (webhook.type === 'customer.subscription.updated' || webhook.type === 'customer.subscription.deleted') {
|
||||
const subscription = webhook.data.object;
|
||||
const subscriptionItem = subscription.items.data[0];
|
||||
@@ -59,7 +73,7 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook:
|
||||
console.error(`Error refunding subscription | ${customer.id}, ${subscription.id}`, error);
|
||||
}
|
||||
|
||||
await sendEmailToCustomer(customer, {
|
||||
await sendEmailToCustomer(mailer, customer, {
|
||||
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}`
|
||||
@@ -72,7 +86,7 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook:
|
||||
}
|
||||
|
||||
const pid = Number(customer.metadata.pnid_pid);
|
||||
const pnid = await database.PNID.findOne({ pid });
|
||||
const pnid = await papr.Pnid.findOne({ pid });
|
||||
|
||||
if (!pnid) {
|
||||
// PNID does not exist
|
||||
@@ -95,7 +109,7 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook:
|
||||
console.error(`Error refunding subscription | ${customer.id}, ${subscription.id} |`, error);
|
||||
}
|
||||
|
||||
await sendEmailToCustomer(customer, {
|
||||
await sendEmailToCustomer(mailer, customer, {
|
||||
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}`
|
||||
@@ -107,47 +121,47 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook:
|
||||
return;
|
||||
}
|
||||
|
||||
const latestWebhookTimestamp = pnid.get('connections.stripe.latest_webhook_timestamp');
|
||||
const latestWebhookTimestamp = pnid.connections?.stripe?.latest_webhook_timestamp;
|
||||
|
||||
if (latestWebhookTimestamp && latestWebhookTimestamp >= webhook.created) {
|
||||
// Do nothing, this webhook is older than the latest seen
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSubscriptionId = pnid.get('connections.stripe.subscription_id');
|
||||
const discordId = pnid.get('connections.discord.id');
|
||||
const currentSubscriptionId = pnid.connections?.stripe?.subscription_id;
|
||||
const discordId = pnid.connections?.discord?.id;
|
||||
|
||||
if (subscription.status === 'canceled' && currentSubscriptionId && subscription.id !== currentSubscriptionId) {
|
||||
// Canceling old subscription, do nothing but update webhook date and remove Discord roles
|
||||
if (product.metadata.beta === 'true') {
|
||||
util.removeDiscordMemberTesterRole(discordId).catch((error) => {
|
||||
await util.removeDiscordMemberTesterRole(discordId).catch((error) => {
|
||||
console.error(`Error removing user Discord tester role | ${customer.id}, ${discordId}, ${pid} |`, error);
|
||||
});
|
||||
}
|
||||
|
||||
util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
await util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
console.error(`Error removing user Discord supporter role | ${customer.id}, ${discordId}, ${pid}, ${product.metadata.discord_role_id} |`, error);
|
||||
});
|
||||
|
||||
const updateData = {
|
||||
'connections.stripe.latest_webhook_timestamp': webhook.created
|
||||
};
|
||||
|
||||
await database.PNID.updateOne({
|
||||
await papr.Pnid.updateOne({
|
||||
pid,
|
||||
'connections.stripe.latest_webhook_timestamp': {
|
||||
$lte: webhook.created
|
||||
}
|
||||
}, { $set: updateData }).exec();
|
||||
}, {
|
||||
$set: {
|
||||
'connections.stripe.latest_webhook_timestamp': webhook.created
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
'connections.stripe.subscription_id': subscription.status === 'active' ? subscription.id : null,
|
||||
'connections.stripe.price_id': subscription.status === 'active' ? subscriptionItem.plan.id : null,
|
||||
const updateData: PaprMatchKeysAndValues<PnidDocument> = {
|
||||
'connections.stripe.subscription_id': subscription.status === 'active' ? subscription.id : undefined,
|
||||
'connections.stripe.price_id': subscription.status === 'active' ? subscriptionItem.plan.id : undefined,
|
||||
'connections.stripe.tier_level': subscription.status === 'active' ? Number(product.metadata.tier_level || 0) : 0,
|
||||
'connections.stripe.tier_name': subscription.status === 'active' ? product.name : null,
|
||||
'connections.stripe.tier_name': subscription.status === 'active' ? product.name : undefined,
|
||||
'connections.stripe.latest_webhook_timestamp': webhook.created
|
||||
};
|
||||
|
||||
@@ -158,7 +172,7 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook:
|
||||
updateData.server_access_level = 'test';
|
||||
}
|
||||
|
||||
util.assignDiscordMemberTesterRole(discordId).catch((error) => {
|
||||
await util.assignDiscordMemberTesterRole(discordId).catch((error) => {
|
||||
console.error(`Error assigning user Discord tester role | ${customer.id}, ${discordId}, ${pid} |`, error);
|
||||
});
|
||||
} else {
|
||||
@@ -169,18 +183,20 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook:
|
||||
updateData.server_access_level = 'prod';
|
||||
}
|
||||
|
||||
util.removeDiscordMemberTesterRole(discordId).catch((error) => {
|
||||
await util.removeDiscordMemberTesterRole(discordId).catch((error) => {
|
||||
console.error(`Error removing user Discord tester role | ${customer.id}, ${discordId}, ${pid} |`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await database.PNID.updateOne({
|
||||
await papr.Pnid.updateOne({
|
||||
pid,
|
||||
'connections.stripe.latest_webhook_timestamp': {
|
||||
$lte: webhook.created
|
||||
}
|
||||
}, { $set: updateData }).exec();
|
||||
}, {
|
||||
$set: updateData
|
||||
});
|
||||
|
||||
if (subscription.status === 'active') {
|
||||
// Get all the customers active subscriptions
|
||||
@@ -203,62 +219,62 @@ export async function handleStripeEvent(event: H3Event, stripe: Stripe, webhook:
|
||||
}
|
||||
}
|
||||
|
||||
await sendEmailToCustomer(customer, {
|
||||
await sendEmailToCustomer(mailer, customer, {
|
||||
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!`
|
||||
})
|
||||
|
||||
util.assignDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
await util.assignDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
console.error(`Error assigning user Discord supporter role | ${customer.id}, ${discordId}, ${pid}, ${product.metadata.discord_role_id} |`, error);
|
||||
});
|
||||
|
||||
await sendToNotificationEmails(notificationEmails, {
|
||||
await sendToNotificationEmails(mailer, notificationEmails, {
|
||||
title: `New ${product.name} subscription`,
|
||||
body: `${pnid.get('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(customer, {
|
||||
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! `
|
||||
})
|
||||
|
||||
util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
await util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
console.error(`Error removing user Discord supporter role | ${customer.id}, ${discordId}, ${pid}, ${product.metadata.discord_role_id} |`, error);
|
||||
});
|
||||
|
||||
await sendToNotificationEmails(notificationEmails, {
|
||||
await sendToNotificationEmails(mailer, notificationEmails, {
|
||||
title: `Canceled ${product.name} subscription`,
|
||||
body: `${pnid.get('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(customer, {
|
||||
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! `
|
||||
})
|
||||
|
||||
util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
await util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
console.error(`Error removing user Discord supporter role | ${customer.id}, ${discordId}, ${pid}, ${product.metadata.discord_role_id} |`, error);
|
||||
});
|
||||
|
||||
await sendToNotificationEmails(notificationEmails, {
|
||||
await sendToNotificationEmails(mailer, notificationEmails, {
|
||||
title: `Removed ${product.name} subscription`,
|
||||
body: `${pnid.get('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(customer, {
|
||||
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!`
|
||||
})
|
||||
|
||||
util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
await util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
console.error(`Error removing user Discord supporter role | ${customer.id}, ${discordId}, ${pid}, ${product.metadata.discord_role_id} |`, error);
|
||||
});
|
||||
|
||||
await sendToNotificationEmails(notificationEmails, {
|
||||
await sendToNotificationEmails(mailer, notificationEmails, {
|
||||
title: `Removed ${product.name} subscription`,
|
||||
body: `${pnid.username}'s ${product.name} tier subscription status has been changed to ${subscription.status}`,
|
||||
})
|
||||
|
||||
27
server/utils/mailer.ts
Normal file
27
server/utils/mailer.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createTransport, Transporter } from "nodemailer";
|
||||
import type { H3Event } from 'h3'
|
||||
|
||||
let transport: Transporter | null = null;
|
||||
|
||||
export function useMailer(event: H3Event): Transporter | null {
|
||||
if (!transport) {
|
||||
const config = useRuntimeConfig(event);
|
||||
if (config.smtpHost && config.smtpPort && config.smtpFromEmail) {
|
||||
transport = createTransport({
|
||||
from: {
|
||||
address: config.smtpFromEmail,
|
||||
name: config.smtpFromName ? config.smtpFromName : undefined,
|
||||
},
|
||||
host: config.smtpHost,
|
||||
port: config.smtpPort,
|
||||
secure: config.smtpSecure,
|
||||
auth: {
|
||||
user: config.smtpUser ? config.smtpUser : undefined,
|
||||
pass: config.smtpPassword ? config.smtpPassword : undefined
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return transport;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Octokit } from "octokit"
|
||||
import type { H3Event } from 'h3'
|
||||
|
||||
let octokit: Octokit | null = null;
|
||||
|
||||
|
||||
46
server/utils/papr.ts
Normal file
46
server/utils/papr.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
import Papr, { schema, types } from "papr"
|
||||
import type { H3Event } from 'h3'
|
||||
|
||||
const papr = new Papr();
|
||||
let conn: MongoClient | null = null;
|
||||
|
||||
const pnidSchema = schema({
|
||||
pid: types.number({ required: true }),
|
||||
username: types.string({ required: true }),
|
||||
access_level: types.number({ required: true }),
|
||||
server_access_level: types.enum(['prod', 'test', 'dev'] as const, { required: true }),
|
||||
connections: types.object({
|
||||
stripe: types.object({
|
||||
subscription_id: types.string(),
|
||||
price_id: types.string(),
|
||||
tier_level: types.number(),
|
||||
tier_name: types.string(),
|
||||
latest_webhook_timestamp: types.number(),
|
||||
}),
|
||||
discord: types.object({
|
||||
id: types.string(),
|
||||
})
|
||||
})
|
||||
});
|
||||
const Pnid = papr.model('pnids', pnidSchema);
|
||||
export type PnidDocument = (typeof pnidSchema)[0];
|
||||
|
||||
const paprInstance = {
|
||||
papr,
|
||||
Pnid,
|
||||
} as const;
|
||||
|
||||
export type PaprInstance = typeof paprInstance;
|
||||
|
||||
export async function usePapr(event: H3Event): Promise<PaprInstance | null> {
|
||||
if (!conn) {
|
||||
const config = useRuntimeConfig(event);
|
||||
if (config.mongoConnectionString) {
|
||||
conn = await MongoClient.connect(config.mongoConnectionString);
|
||||
papr.initialize(conn.db());
|
||||
}
|
||||
}
|
||||
|
||||
return conn ? paprInstance : null;
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { z, ZodType } from 'zod';
|
||||
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);
|
||||
if (!body.success) {
|
||||
throw createError({
|
||||
status: 400,
|
||||
statusText: 'Invalid input'
|
||||
message: 'Invalid input'
|
||||
});
|
||||
}
|
||||
return body.data;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Stripe } from "stripe"
|
||||
import type { H3Event } from 'h3'
|
||||
|
||||
let stripe: Stripe | null = null;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createChannel, createClient, Metadata } from 'nice-grpc';
|
||||
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'
|
||||
|
||||
let grpc: { channel: Channel } | null = null;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ async function loginSubmission() {
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof FetchError) {
|
||||
errorMessage.value = error.statusText;
|
||||
errorMessage.value = error.message;
|
||||
} else {
|
||||
errorMessage.value = `Error during login: ${error}`; // TODO: localize
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ async function registerSubmission() {
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof FetchError) {
|
||||
errorMessage.value = error.statusText;
|
||||
errorMessage.value = error.message;
|
||||
} else {
|
||||
if (error === 'challenge-closed') { // Thrown if the captcha is closed, can be safely ignored
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user