Skip to content
This repository has been archived by the owner on Jul 21, 2022. It is now read-only.

Commit

Permalink
Merge pull request #116 from Em1tt/announcement-pr
Browse files Browse the repository at this point in the history
Added new API endpoints + Updated API endpoints
  • Loading branch information
FireMario211 authored Aug 7, 2021
2 parents a80690b + 267d88e commit 3b108eb
Show file tree
Hide file tree
Showing 12 changed files with 220 additions and 20 deletions.
31 changes: 26 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@
"mailparser": "^3.2.0",
"morgan": "^1.10.0",
"ms": "^2.1.2",
"node-fetch": "^2.6.1",
"nodemailer": "^6.5.0",
"smtp-server": "^3.9.0"
"smtp-server": "^3.9.0",
"stripe": "^8.167.0"
},
"devDependencies": {
"@types/express": "^4.17.12",
Expand Down
90 changes: 90 additions & 0 deletions src/modules/api/announcements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* API for Showing announcements (or posting)
*/
import express from 'express';
import { sql } from '../sql';
import { permissions } from '../permissions'
import { auth } from './auth';

function encode_base64(str) {
if (!str.length) return false;
return btoa(encodeURIComponent(str));
}
function decode_base64(str) {
if (!str.length) return false;
return decodeURIComponent(atob(str));
}

export const prop = {
name: "announcements",
desc: "API for Announcements",
run: async (req: express.Request, res: express.Response): Promise<any> => {
const allowedMethods = ["GET", "POST", "DELETE"];
res.set("Allow", allowedMethods.join(", ")); // To give the method of whats allowed
if (!allowedMethods.includes(req.method)) return res.sendStatus(405);
const params = req.params[0].split("/").slice(1);
let userData = await auth.verifyToken(req, res, false, "both");
if (userData == 101) {
const newAccessToken = await auth.regenAccessToken(req, res);
if (typeof newAccessToken != "string") return false;
userData = await auth.verifyToken(req, res, false, "both")
}
if (typeof userData != "object" && req.method != "GET") return res.sendStatus(userData);
switch (req.method) {
case "GET": { // Fetching the latest announcement.
let type = req.query.type; // Announcement Type
if (!type) type = "null";
if (!["outage", "news", "warning", "null"].includes(type.toString().toLowerCase())) return res.sendStatus(406);
const params: Array<any> = [Date.now()]; // ESLint wont stop complaining about this.
let query = "SELECT announcementText, showToCustomersOnly, announcementType FROM announcements WHERE deleteIn > ?";
if (type != "null") {
query += " AND announcementType = ?"
params.push(type);
} else if (typeof userData != "object") {
query += " AND showToCustomersOnly = 0"
}
const announcements = await sql.db.prepare(query).all(params);
if (!announcements.length) return res.sendStatus(404); // Announcement not found
if (announcements.showToCustomersOnly && typeof userData != "object") return res.sendStatus(403); // Forbidden from viewing announcement.
return res.status(200).json(announcements.map(announcement => {
announcement.announcementText = decode_base64(announcement.announcementText);
return announcement;
}));
}
case "POST": { // For creating an announcement.
if (!permissions.hasPermission(userData['permission_id'], `/announcements`)) return res.sendStatus(403);
const announcement = req.body.text; // Announcement Text
const type = req.body.type; // Announcement Type
let deleteOn = req.body.deleteOn; // When to delete the announcement
let showCustomers = req.body.showToCustomersOnly; // If it should only show to customers
deleteOn = parseInt(deleteOn);
if (!announcement && !type && !deleteOn && !showCustomers) return res.sendStatus(406);
if (!["outage", "news", "warning"].includes(type.toString())) return res.status(406).send("Invalid type");
const currentDate = Date.now();
if (deleteOn < currentDate) return res.status(406).send(`Invalid Timestamp (Cannot be higher than ${currentDate})`);
if (isNaN(deleteOn)) return res.status(406).send("Invalid Timestamp")
if (!["0","1","true","false"].includes(showCustomers.toString())) return res.status(406).send("Show Customers must be true or false.")
switch (showCustomers.toString()) {
case "true": showCustomers = 1; break;
case "false": showCustomers = 0; break;
}
await sql.db.prepare(`INSERT INTO announcements
(announcementType, announcementText, deleteIn, showToCustomersOnly) VALUES
(?, ?, ?, ?)`).run(type, encode_base64(announcement), deleteOn, showCustomers);
const getAnnouncementID = await sql.db.prepare('SELECT announcement_id FROM announcements WHERE deleteIn = ? AND announcementType = ? AND announcementText = ?')
.get(deleteOn, type, encode_base64(announcement));
if (!getAnnouncementID) return res.sendStatus(204);
return res.status(200).json(getAnnouncementID)
}
case "DELETE": { // For deleting an announcement.
if (!permissions.hasPermission(userData['permission_id'], `/announcements`)) return res.sendStatus(403);
const id = req.query.type;
if (!id) return res.sendStatus(406);
const findAnnouncement = sql.db.prepare('SELECT count(*) FROM announcements WHERE announcement_id = ?').pluck().get(id);
if (!findAnnouncement) return res.sendStatus(404); // Announcement not found.
await sql.db.prepare('DELETE FROM announcements WHERE announcement_id = ?').run(id);
return res.sendStatus(204);
}
}
}
}
57 changes: 54 additions & 3 deletions src/modules/api/order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import express from 'express';
import { sql } from '../sql';
import { auth } from './auth';
const stripe = require('stripe')(process.env.STRIPE_SK_TEST); // This looks bad but its required.

// stripe listen --forward-to localhost:3000/api/order/webhook

export const prop = {
name: "order",
Expand All @@ -19,11 +22,59 @@ export const prop = {
if (typeof newAccessToken != "string") return false;
userData = await auth.verifyToken(req, res, false, "both")
}
if (typeof userData != "object") return res.sendStatus(userData);
const paramName = params[0]
if (typeof userData != "object" && paramName != "webhook") return res.sendStatus(userData);
switch (paramName) {
case "checkout": { // Currently not finished
break;
case "checkout": {
res.set("Allow", "POST");
if (req.method != "POST") return res.sendStatus(405);
const session = await stripe.checkout.sessions.create({
customer_email: userData['email'],
success_url: `http://${req.get('host')}/billing/success`,
cancel_url: `http://${req.get('host')}/billing/cancel`,
payment_method_types: ['card'],
line_items: [
{price: '', quantity: 1},
],
mode: 'subscription',
});
return res.redirect(303, session.url)
}
case "webhook": { // May switch to /api/stripe/webhook instead.
const endpointSecret = process.env.WEBHOOK_SECRET
const payload = req.body;

const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(payload, sig, endpointSecret);
} catch (err) {
console.log(err.message)
return res.status(400).send(`Webhook Error: ${err.message}`);
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
console.log("create order");
if (session.payment_status === 'paid') {
console.log("fulfill order (create VPS)");
}
break;
}

case 'checkout.session.async_payment_succeeded': {
const session = event.data.object;
console.log("fulfill order (create VPS)");
break;
}

case 'checkout.session.async_payment_failed': {
const session = event.data.object;
console.log("email customer saying to retry order");
break;
}
}
return res.sendStatus(200);
}
case "coupon": {
res.set("Allow", "GET");
Expand Down
23 changes: 22 additions & 1 deletion src/modules/api/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import express from 'express';
import { sql } from '../sql';
import { auth } from './auth';
import fetch from 'node-fetch';

export const prop = {
name: "register",
Expand All @@ -13,7 +14,27 @@ export const prop = {
if (req.method != "POST") return res.sendStatus(405) // If the request isn't POST then respond with Method not Allowed.
const { name, email, password } = req.body;
if ([name, email, password].includes(undefined)) return res.status(406)
.send("Please enter in a Name, Email, and Password.");
.send("Please enter in a Name, Email, and Password.");

function recaptcha() {
const key = process.env.RECAPTCHA_SECRET;
return new Promise((resolve, reject) => { // Promises are great.
const response = req.body["g-recaptcha-response"];
fetch(`https://www.google.com/recaptcha/api/siteverify?secret=${key}&response=${response}`, {
method: 'POST',
}).then(resp => resp.json())
.then(json => resolve(json))
.catch(e => reject(e));
})
}
/*
<script src="https://www.recaptcha.net/recaptcha/api.js" async defer></script>
<div class="g-recaptcha brochure__form__captcha"
data-sitekey="PUBLIC_KEY">
</div>
*/
const recaptcha_response = await recaptcha();
if (!recaptcha_response || (recaptcha_response && !recaptcha_response["success"])) return res.status(403).send("Recaptcha failed!");
const userExists = await sql.db.prepare("SELECT count(*) FROM users WHERE email = ?")
.pluck().get(email); // Checks if the user exists.
if (userExists) return res.status(409)
Expand Down
1 change: 1 addition & 0 deletions src/modules/views/billing/html/cancel.eta
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Cancel Page
7 changes: 5 additions & 2 deletions src/modules/views/billing/html/order.eta
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
</form>
</div>
<div>
<form>
<form action="/api/order/checkout" method="POST">
<p>Coupons & Checkout</p>
<fieldset class="coupon">
<label>Coupon code<span class="tooltip">?<span class="tooltiptext">Apply discount codes to get even better deals!</span></span></label>
Expand All @@ -110,7 +110,10 @@
<p class="small">Recurring Amount: €<span class="fullPrice recurring"><%=it.details.price%></span></p>
<p class="price">Due Today: €<span class="fullPrice"><%=it.details.price%></span></p>
<p class="small">+ VAT calculated at checkout*</p>
<a id="checkout">Checkout with Stripe</a>
<!--<form action="/api/order/redirect" method="POST">-->
<!--<a id="checkout">Checkout with Stripe</a>-->
<button id="checkout" type="submit">Checkout with Stripe</button>
<!--</form>-->
</form>
</div>
</main>
Expand Down
1 change: 1 addition & 0 deletions src/modules/views/billing/html/success.eta
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Success Page
3 changes: 2 additions & 1 deletion src/modules/views/billing/js/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ function onLoad() {
registerUser({
name,
email,
password,
password//,
//"g-recaptcha-response": grecaptcha.getResponse()
});
});
try{
Expand Down
10 changes: 5 additions & 5 deletions src/modules/website.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import config from "../config.json";
import { util } from "../util";
import { Endpoint } from "../types/endpoint";
import helmet from "helmet"
import bodyParser from "body-parser"
import cookieParser from "cookie-parser"
import { auth } from "./api/auth"
import * as plans from "../plans.json"
Expand Down Expand Up @@ -36,9 +35,10 @@ app.use(morgan("[express]\t:method :url :status :res[content-length] - :response
app.use(express.static(path.join(__dirname, "views")));

// Create Parse for application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false })) // Required for req.body
app.use(express.urlencoded({ extended: false })) // Required for req.body
app.use('/api/order/webhook', express.raw({type: 'application/json'}));
// Create Parse for application/json
app.use(bodyParser.json())
app.use(express.json())
// Create Parse for Cookies

// Using Helmet to mitigate common security issues via setting HTTP Headers, such as XSS Protect and setting X-Frame-Options to sameorigin, meaning it'll prevent iframe attacks
Expand All @@ -47,8 +47,8 @@ app.use(
contentSecurityPolicy: {
useDefaults: true, // nonce when
directives: {
defaultSrc: ["'self'"],
"script-src": ["'self'", "'unsafe-inline'", "static.cloudflareinsights.com", "unpkg.com", "cdn.jsdelivr.net", "ajax.googleapis.com", "*.gstatic.com", "pixijs.download", "'unsafe-eval'"], // unsafe eval worst idea, pixijs why do you have this
defaultSrc: ["'self'", "www.recaptcha.net"],
"script-src": ["'self'", "'unsafe-inline'", "static.cloudflareinsights.com", "unpkg.com", "www.recaptcha.net", "cdn.jsdelivr.net", "ajax.googleapis.com", "*.gstatic.com", "js.stripe.com", "pixijs.download", "'unsafe-eval'"], // unsafe eval worst idea, pixijs why do you have this
"style-src": ["'self'", "'unsafe-inline'", "unpkg.com", "fonts.googleapis.com", "*.gstatic.com", "use.fontawesome.com", "fontawesome.com"],
"script-src-attr": ["'self'", "'unsafe-inline'"]
}
Expand Down
2 changes: 1 addition & 1 deletion src/permissions.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"accessAPI": ["/tickets", "/tickets/create", "/tickets/list", "/tickets/:ticketid", "/mail"]
},
"4": {
"accessAPI": ["/tickets", "/tickets/create", "/tickets/list", "/tickets/:ticketid", "/tickets/:ticketid/delete", "/mail"]
"accessAPI": ["/tickets", "/tickets/create", "/tickets/list", "/tickets/:ticketid", "/tickets/:ticketid/delete", "/mail", "/announcements"]
}

}
11 changes: 10 additions & 1 deletion src/sql/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,13 @@ CREATE TABLE IF NOT EXISTS coupons (
value INTEGER NOT NULL DEFAULT 0.25, -- Value (percentage) that should be taken off.
forever INTEGER NOT NULL DEFAULT 0, -- If this coupon should apply in reoccuring payments or not. (0 = False | 1 = True)
createdIn TIMESTAMP NOT NULL -- When the coupon was created
);
);

CREATE TABLE IF NOT EXISTS announcements (
announcement_id INTEGER NOT NULL PRIMARY KEY, -- Announcement ID.
announcementType TEXT NOT NULL DEFAULT 'news', -- Announcement Type ("outage", "news", "warning")
announcementText TEXT NOT NULL DEFAULT 'Announcement', -- What the text should show
deleteIn TIMESTAMP NOT NULL, -- When the announcement should be deleted (or invalid)
showToCustomersOnly INTEGER NOT NULL DEFAULT 0 -- If it should only show to users who are logged in (0 = False | 1 = True)
);

0 comments on commit 3b108eb

Please sign in to comment.