pull/29/head
Matthew 2020-11-08 04:34:49 -05:00
parent 4578819c1a
commit cad3e234b9
No known key found for this signature in database
GPG Key ID: 210AF32ADE3B5C4B
14 changed files with 253 additions and 10 deletions

View File

@ -46,6 +46,7 @@
"nodemailer": "^6.4.8",
"pluris": "^0.2.5",
"signale": "^1.4.0",
"stripe": "^8.120.0",
"uuid": "^8.0.0",
"yaml": "^1.9.2"
}

View File

@ -1,6 +1,6 @@
import { Server, ServerManagement } from '../../class';
export default (management: ServerManagement) => {
const server = new Server(management, 3890, `${__dirname}/routes`);
const server = new Server(management, 3890, `${__dirname}/routes`, false);
return server;
};

View File

@ -1,7 +1,6 @@
import { TextChannel } from 'eris';
/* eslint-disable no-shadow */
import jwt from 'jsonwebtoken';
import { RichEmbed, Route, Server, LocalStorage } from '../../../class';
import axios from 'axios';
import bodyParser from 'body-parser';
import { Route, Server, LocalStorage } from '../../../class';
// import acknowledgements from '../../../configs/acknowledgements.json';
export default class Internal extends Route {
@ -79,5 +78,42 @@ export default class Internal extends Route {
return this.handleError(err, res);
}
});
this.router.post('/sub', bodyParser.raw({ type: 'application/json' }), async (req, res) => {
try {
const event = this.server.client.stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], this.server.client.config.stripeSubSigningSecret);
const data = <any> event.data.object;
switch (event.type) {
default:
return res.sendStatus(400);
case 'customer.subscription.created':
if (data.items.data[0].price.product === 'prod_Hi4EYmf2am5VZt') {
const customer = await this.server.client.db.Customer.findOne({ cusID: data.customer }).lean().exec();
if (!customer) return res.sendStatus(404);
await axios({
method: 'get',
url: `https://api.libraryofcode.org/wh/t2?userID=${customer.userID}&auth=${this.server.client.config.internalKey}`,
});
res.sendStatus(200);
}
break;
case 'customer.subscription.deleted':
if (data.items.data[0].price.product === 'prod_Hi4EYmf2am5VZt') {
const customer = await this.server.client.db.Customer.findOne({ cusID: data.customer }).lean().exec();
if (!customer) return res.sendStatus(404);
await axios({
method: 'get',
url: `https://api.libraryofcode.org/wh/t2-rm?userID=${customer.userID}&auth=${this.server.client.config.internalKey}`,
});
return res.sendStatus(200);
}
break;
}
return null;
} catch (err) {
return this.handleError(err, res);
}
});
}
}

View File

@ -12,6 +12,43 @@ export default class Root extends Route {
public bind() {
this.router.get('/', (_req, res) => res.redirect('https://www.libraryofcode.org/'));
this.router.get('/dash', async (req, res) => {
try {
const lookup = await this.server.client.db.CustomerPortal.findOne({ key: req.query.q?.toString() });
if (!lookup) return res.status(404).json({ code: this.constants.codes.NOT_FOUND, message: this.constants.messages.NOT_FOUND });
if (new Date(lookup.expiresOn) < new Date()) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED });
const customer = await this.server.client.db.Customer.findOne({ userID: lookup.userID });
if (!customer) {
const newCus = await this.server.client.stripe.customers.create({
email: lookup.emailAddress,
metadata: {
userID: lookup.userID,
username: lookup.username,
},
});
await (new this.server.client.db.Customer({
cusID: newCus.id,
userID: lookup.userID,
})).save();
const billingURL = await this.server.client.stripe.billingPortal.sessions.create({
customer: newCus.id,
return_url: 'https://www.libraryofcode.org',
});
res.redirect(302, billingURL.url);
return await lookup.updateOne({ $set: { used: true } });
}
const billingURL = await this.server.client.stripe.billingPortal.sessions.create({
customer: customer.cusID,
return_url: 'https://www.libraryofcode.org',
});
res.redirect(302, billingURL.url);
return await lookup.updateOne({ $set: { used: true } });
} catch (err) {
return this.handleError(err, res);
}
});
this.router.get('/:key', async (req, res) => {
try {
const link: RedirectRaw = await this.server.client.db.Redirect.findOne({ key: req.params.key }).lean().exec();

View File

@ -1,9 +1,10 @@
import Stripe from 'stripe';
import eris from 'eris';
import pluris from 'pluris';
import mongoose from 'mongoose';
import { promises as fs } from 'fs';
import { Collection, Command, LocalStorage, Queue, Util, ServerManagement, Event } from '.';
import { File, FileInterface, Member, MemberInterface, Merchant, MerchantInterface, Moderation, ModerationInterface, NNTrainingData, NNTrainingDataInterface, Note, NoteInterface, PagerNumber, PagerNumberInterface, Rank, RankInterface, Redirect, RedirectInterface, Score, ScoreInterface, ScoreHistorical, ScoreHistoricalInterface, Staff, StaffInterface, Stat, StatInterface } from '../models';
import { Customer, CustomerInterface, CustomerPortal, CustomerPortalInterface, File, FileInterface, Member, MemberInterface, Merchant, MerchantInterface, Moderation, ModerationInterface, NNTrainingData, NNTrainingDataInterface, Note, NoteInterface, PagerNumber, PagerNumberInterface, Rank, RankInterface, Redirect, RedirectInterface, Score, ScoreInterface, ScoreHistorical, ScoreHistoricalInterface, Staff, StaffInterface, Stat, StatInterface } from '../models';
import { Config } from '../../types'; // eslint-disable-line
pluris(eris);
@ -23,7 +24,9 @@ export default class Client extends eris.Client {
public queue: Queue;
public db: { File: mongoose.Model<FileInterface>, Member: mongoose.Model<MemberInterface>, Merchant: mongoose.Model<MerchantInterface>, Moderation: mongoose.Model<ModerationInterface>, NNTrainingData: mongoose.Model<NNTrainingDataInterface>, Note: mongoose.Model<NoteInterface>, PagerNumber: mongoose.Model<PagerNumberInterface>, Rank: mongoose.Model<RankInterface>, Redirect: mongoose.Model<RedirectInterface>, Score: mongoose.Model<ScoreInterface>, ScoreHistorical: mongoose.Model<ScoreHistoricalInterface>, Staff: mongoose.Model<StaffInterface>, Stat: mongoose.Model<StatInterface>, local: { muted: LocalStorage } };
public stripe: Stripe;
public db: { Customer: mongoose.Model<CustomerInterface>, CustomerPortal: mongoose.Model<CustomerPortalInterface>, File: mongoose.Model<FileInterface>, Member: mongoose.Model<MemberInterface>, Merchant: mongoose.Model<MerchantInterface>, Moderation: mongoose.Model<ModerationInterface>, NNTrainingData: mongoose.Model<NNTrainingDataInterface>, Note: mongoose.Model<NoteInterface>, PagerNumber: mongoose.Model<PagerNumberInterface>, Rank: mongoose.Model<RankInterface>, Redirect: mongoose.Model<RedirectInterface>, Score: mongoose.Model<ScoreInterface>, ScoreHistorical: mongoose.Model<ScoreHistoricalInterface>, Staff: mongoose.Model<StaffInterface>, Stat: mongoose.Model<StatInterface>, local: { muted: LocalStorage } };
constructor(token: string, options?: eris.ClientOptions) {
super(token, options);
@ -31,7 +34,7 @@ export default class Client extends eris.Client {
this.events = new Collection<Event>();
this.intervals = new Collection<NodeJS.Timeout>();
this.queue = new Queue(this);
this.db = { File, Member, Merchant, Moderation, NNTrainingData, Note, PagerNumber, Rank, Redirect, Score, ScoreHistorical, Staff, Stat, local: { muted: new LocalStorage('muted') } };
this.db = { Customer, CustomerPortal, File, Member, Merchant, Moderation, NNTrainingData, Note, PagerNumber, Rank, Redirect, Score, ScoreHistorical, Staff, Stat, local: { muted: new LocalStorage('muted') } };
}
@ -60,6 +63,7 @@ export default class Client extends eris.Client {
public loadPlugins() {
this.util = new Util(this);
this.serverManagement = new ServerManagement(this);
this.stripe = new Stripe(this.config.stripeKey, { apiVersion: null, typescript: true });
}
public async loadIntervals() {

View File

@ -15,13 +15,17 @@ export default class Server {
private root: string;
constructor(parent: ServerManagement, port: number, routeRoot: string) {
protected parse: boolean;
constructor(parent: ServerManagement, port: number, routeRoot: string, parse = true) {
this.parent = parent;
this.app = express();
this.routes = new Collection<Route>();
this.port = port;
this.root = routeRoot;
this.parse = parse;
this.init();
this.loadRoutes();
}
@ -50,7 +54,9 @@ export default class Server {
}
public init() {
this.app.use(bodyParser.json());
if (this.parse) {
this.app.use(bodyParser.json());
}
this.app.set('trust proxy', 'loopback');
this.app.use(helmet({
hsts: false,

55
src/commands/billing.ts Normal file
View File

@ -0,0 +1,55 @@
import axios from 'axios';
import moment from 'moment';
import { Message } from 'eris';
import { randomBytes } from 'crypto';
import { v4 as uuid } from 'uuid';
import { Client, Command } from '../class';
import Billing_T2 from './billing_t2';
export default class Billing extends Command {
constructor(client: Client) {
super(client);
this.name = 'billing';
this.description = 'Pulls up your Billing Portal. You must have a CS Account to continue.';
this.usage = `${this.client.config.prefix}billing`;
this.subcmds = [Billing_T2];
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message) {
try {
const response = <{
found: boolean,
emailAddress?: string,
tier?: number,
supportKey?: string,
}> (await axios.get(`https://api.cloud.libraryofcode.org/wh/info?id=${message.author.id}&authorization=${this.client.config.internalKey}`)).data;
if (!response.found) return this.error(message.channel, 'CS Account not found.');
const portalKey = randomBytes(50).toString('hex');
const uid = uuid();
const redirect = new this.client.db.Redirect({
key: uid,
to: `https://loc.sh/dash?q=${portalKey}`,
});
const portal = new this.client.db.CustomerPortal({
key: portalKey,
username: message.author.username,
userID: message.author.id,
emailAddress: response.emailAddress,
expiresOn: moment().add(5, 'minutes').toDate(),
used: false,
});
await portal.save();
await redirect.save();
const chan = await this.client.getDMChannel(message.author.id);
await chan.createMessage(`__***Billing Account Portal***__\nClick here: https://loc.sh/${uid}\n\nYou will be redirected to your billing portal, please note the link expires after 5 minutes.`);
return await this.success(message.channel, 'Your Billing Portal information has been DMed to you.');
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

View File

@ -0,0 +1,47 @@
import axios from 'axios';
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Billing_T2 extends Command {
constructor(client: Client) {
super(client);
this.name = 't2';
this.description = 'Subscription to CS Tier 2.';
this.usage = `${this.client.config.prefix}billing t2`;
this.permissions = 0;
this.guildOnly = false;
this.enabled = true;
}
public async run(message: Message) {
try {
const response = <{
found: boolean,
emailAddress?: string,
tier?: number,
supportKey?: string,
}> (await axios.get(`https://api.cloud.libraryofcode.org/wh/info?id=${message.author.id}&authorization=${this.client.config.internalKey}`)).data;
if (!response.found) return this.error(message.channel, 'CS Account not found.');
const customer = await this.client.db.Customer.findOne({ userID: message.author.id });
if (!customer) return this.error(message.channel, `You do not have a Customer Account. Please run \`${this.client.config.prefix}billing\`, once you visit the Billing Portal via the URL given to you, please try again.`);
const subscription = await this.client.stripe.subscriptions.create({
customer: customer.cusID,
payment_behavior: 'allow_incomplete',
items: [{ price: 'price_1H8e6ODatwI1hQ4WFVvX6Nda' }],
days_until_due: 1,
collection_method: 'send_invoice',
});
await this.client.stripe.invoices.finalizeInvoice(subscription.latest_invoice.toString());
const invoice = await this.client.stripe.invoices.retrieve(subscription.latest_invoice.toString());
const chan = await this.client.getDMChannel(message.author.id);
await chan.createMessage(`__**Invoice for New Subscription**__\n${invoice.hosted_invoice_url}\n\n*Please click on the link above to pay for your subscription.*`);
return this.success(message.channel, 'Transaction processed.');
} catch (err) {
return this.client.util.handleError(err, message, this);
}
}
}

View File

@ -5,6 +5,7 @@ export { default as addrank } from './addrank';
export { default as addredirect } from './addredirect';
export { default as apply } from './apply';
export { default as ban } from './ban';
export { default as billing } from './billing';
export { default as delitem } from './delitem';
export { default as delmerchant } from './delmerchant';
export { default as delnote } from './delnote';

13
src/models/Customer.ts Normal file
View File

@ -0,0 +1,13 @@
import { Document, Schema, model } from 'mongoose';
export interface CustomerInterface extends Document {
cusID: string,
userID: string,
}
const Customer: Schema = new Schema({
cusID: String,
userID: String,
});
export default model<CustomerInterface>('Customer', Customer);

View File

@ -0,0 +1,21 @@
import { Document, Schema, model } from 'mongoose';
export interface CustomerPortalInterface extends Document {
key: string,
username: string,
userID: string,
emailAddress: string,
expiresOn: Date,
used?: boolean,
}
const CustomerPortal: Schema = new Schema({
key: String,
username: String,
userID: String,
emailAddress: String,
expiresOn: Date,
used: Boolean,
});
export default model<CustomerPortalInterface>('CustomerPortal', CustomerPortal);

View File

@ -1,3 +1,5 @@
export { default as Customer, CustomerInterface } from './Customer';
export { default as CustomerPortal, CustomerPortalInterface } from './CustomerPortal';
export { default as File, FileInterface } from './File';
export { default as Member, MemberInterface } from './Member';
export { default as Merchant, MerchantInterface } from './Merchant';

2
types/index.d.ts vendored
View File

@ -7,4 +7,6 @@ export declare interface Config {
webhookID: string;
webhookToken: string;
internalKey: string;
stripeKey: string;
stripeSubSigningSecret: string;
}

View File

@ -155,6 +155,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.1.tgz#5d93e0a099cd0acd5ef3d5bde3c086e1f49ff68c"
integrity sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA==
"@types/node@>=8.1.0":
version "14.14.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.6.tgz#146d3da57b3c636cc0d1769396ce1cfa8991147f"
integrity sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==
"@types/nodemailer@^6.4.0":
version "6.4.0"
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.0.tgz#d8c039be3ed685c4719a026455555be82c124b74"
@ -2637,6 +2642,11 @@ qs@6.7.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@^6.6.0:
version "6.9.4"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@ -3183,6 +3193,14 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
stripe@^8.120.0:
version "8.120.0"
resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.120.0.tgz#234a2fd7352c529532d32e76c75094068690a373"
integrity sha512-OzqyUWwdYPla1Onjn94pdGwqpVsOAOlNwo75Yr3T3n1eN17CMUclAVU9hIAqBrDVeyHOyriYklLRhT3NGr7BNw==
dependencies:
"@types/node" ">=8.1.0"
qs "^6.6.0"
supports-color@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"