From ccfcc57540b9da3a28dc2438dc74fd6b79301cda Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Mon, 14 Sep 2020 22:45:32 -0700 Subject: [PATCH] Refactor queries and add auth on streamer endpoints - Move prepared statement definitions to queries module - Add authentication to streamer-only endpoints --- src/app.ts | 80 ++++++++++++++++++++++++++++---------------- src/queries.ts | 89 +++++++++++++++++++++++++++++++++++++++++++++++++ src/requests.ts | 86 +++++++---------------------------------------- 3 files changed, 152 insertions(+), 103 deletions(-) create mode 100644 src/queries.ts diff --git a/src/app.ts b/src/app.ts index de650de..edc6e2a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,7 @@ import * as config from "./config"; import * as requests from "./requests"; import * as twitch from "./twitch"; +import * as queries from "./queries"; import { URLSearchParams } from "url"; import express from "express"; import session from "express-session"; @@ -84,8 +85,19 @@ app.post("/api/addRequest", async (request, response) => { .catch((e: any) => errorHandler(request,response,e)); }); -app.post("/api/updateRequestState", async (request, response) => { // TODO: Streamer auth - response.type('text/plain'); +app.post("/api/updateRequestState", async (request, response) => { + if (request.session) await validateApiToken(request.session); + if (!request.session || !request.session.user) { + response.status(401); + response.send("Must be logged in"); + return; + } + var streamerid = await db.query(queries.getStreamerId).then((result: pg.QueryResult) => result.rows[0]['userid']); + if (request.session.user.id != streamerid) { + response.status(401); + response.send("You are not the streamer"); + return; + } if (!request.body.url) { response.status(400); response.send("Missing url"); @@ -93,11 +105,12 @@ app.post("/api/updateRequestState", async (request, response) => { // TODO: Stre } if (!request.body.state) { response.status(400); - response.send("Missing scoreDiff"); + response.send("Missing state"); return; } var url = request.body.url as string; var state = request.body.state as string; + response.type('text/plain'); requests.updateRequestState(url,state).then((val: [number,string]) => { response.status(val[0]); response.send(val[1]); @@ -105,8 +118,19 @@ app.post("/api/updateRequestState", async (request, response) => { // TODO: Stre .catch((e: any) => errorHandler(request,response,e)); }); -app.post("/api/updateRequestScore", async (request, response) => { // TODO: Streamer auth - response.type('text/plain'); +app.post("/api/updateRequestScore", async (request, response) => { + if (request.session) await validateApiToken(request.session); + if (!request.session || !request.session.user) { + response.status(401); + response.send("Must be logged in"); + return; + } + var streamerid = await db.query(queries.getStreamerId).then((result: pg.QueryResult) => result.rows[0]['userid']); + if (request.session.user.id != streamerid) { + response.status(401); + response.send("You are not the streamer"); + return; + } if (!request.body.url) { response.status(400); response.send("Missing url"); @@ -119,6 +143,7 @@ app.post("/api/updateRequestScore", async (request, response) => { // TODO: Stre } var url = request.body.url as string; var scoreDiff = parseInt(request.body.scoreDiff as string, 10); + response.type('text/plain'); requests.updateRequestScore(url,scoreDiff).then((val: [number,string]) => { response.status(val[0]); response.send(val[1]); @@ -126,14 +151,26 @@ app.post("/api/updateRequestScore", async (request, response) => { // TODO: Stre .catch((e: any) => errorHandler(request,response,e)); }); -app.post("/api/deleteRequest", async (request, response) => { // TODO: Streamer auth - response.type('text/plain'); +app.post("/api/deleteRequest", async (request, response) => { + if (request.session) await validateApiToken(request.session); + if (!request.session || !request.session.user) { + response.status(401); + response.send("Must be logged in"); + return; + } + var streamerid = await db.query(queries.getStreamerId).then((result: pg.QueryResult) => result.rows[0]['userid']); + if (request.session.user.id != streamerid) { + response.status(401); + response.send("You are not the streamer"); + return; + } if (!request.body.url) { response.status(400); response.send("Missing url"); return; } var url = request.body.url as string; + response.type('text/plain'); requests.deleteRequest(url).then((val: [number,string]) => { response.status(val[0]); response.send(val[1]); @@ -204,28 +241,17 @@ app.get("/callback", async (request, response) => { if (typeof tokenResponse == 'undefined') throw new Error('tokenResponse is undefined'); request.session.tokenpair = { access_token: tokenResponse.access_token, refresh_token: tokenResponse.refresh_token }; request.session.user = (await twitch.apiRequest(request.session.tokenpair,"/users")).data[0]; - const updateUserQuery = { - name: "updateUser", - text: "INSERT INTO users (userid,displayName,imageUrl) VALUES ($1,$2,$3)\ - ON CONFLICT (userid) DO UPDATE SET displayName = $2, imageUrl = $3" - } - var query = Object.assign(updateUserQuery,{ values: [request.session.user.id,request.session.user.display_name,request.session.user.profile_image_url] }); + var query = Object.assign(queries.updateUser,{ values: [request.session.user.id,request.session.user.display_name,request.session.user.profile_image_url] }); db.query(query); + var streamerid = await db.query(queries.getStreamerId).then((result: pg.QueryResult) => result.rows[0]['userid']); if (typeof (tokenResponse as any).scope != 'undefined') { // Scopes requested - update streamer info - const getStreamerIdQuery = { - name: "getStreamerId", - text: "SELECT userid FROM streamer" - } - var streamerid = await db.query(getStreamerIdQuery).then((result: pg.QueryResult) => result.rows[0]['userid']); if (request.session.user.id == streamerid) { - const updateStreamerQuery = { - name: "updateStreamer", - text: "INSERT INTO streamer (userid,tokenPair) VALUES ($1,$2)\ - ON CONFLICT (userid) DO UPDATE SET tokenPair = $2" - } - var query = Object.assign(updateStreamerQuery,{ values: [request.session.user.id,JSON.stringify(request.session.tokenpair)] }); + var query = Object.assign(queries.updateStreamer,{ values: [request.session.user.id,JSON.stringify(request.session.tokenpair)] }); db.query(query); } + } else if (request.session.user.id == streamerid) { + response.redirect(307, `https://id.twitch.tv/oauth2/authorize?client_id=${config.twitchClientId}&redirect_uri=${config.urlPrefix}/callback&response_type=code&scope=channel:read:subscriptions moderation:read`); + return; } response.redirect(307, '/'); }); @@ -233,11 +259,7 @@ app.get("/callback", async (request, response) => { // Frontend templates app.get("/", async (request, response) => { if (request.session) await validateApiToken(request.session); - const getStreamerInfoQuery = { - name: "getStreamerInfo", - text: "SELECT displayname,imageurl FROM streamer_user_vw" - } - var streamerInfo = await db.query(getStreamerInfoQuery).then((result: pg.QueryResult) => result.rows[0]);; + var streamerInfo = await db.query(queries.getStreamerInfo).then((result: pg.QueryResult) => result.rows[0]); if (!request.session || !request.session.user) { response.render('main.eta', { loggedIn: false, diff --git a/src/queries.ts b/src/queries.ts new file mode 100644 index 0000000..4f778c9 --- /dev/null +++ b/src/queries.ts @@ -0,0 +1,89 @@ +// User-related queries +export const updateUser = { + name: "updateUser", + text: "INSERT INTO users (userid,displayName,imageUrl) VALUES ($1,$2,$3)\ + ON CONFLICT (userid) DO UPDATE SET displayName = $2, imageUrl = $3" +} + +export const insertBan = { + name: "insertBan", + text: "INSERT INTO bans (userid) VALUES ($1)" +} + +// Streamer-related queries +export const getStreamerId = { + name: "getStreamerId", + text: "SELECT userid FROM streamer" +} + +export const getStreamerIdToken = { + name: "getStreamerIdToken", + text: "SELECT userid,tokenpair FROM streamer" +} + +export const getStreamerInfo = { + name: "getStreamerInfo", + text: "SELECT displayname,imageurl FROM streamer_user_vw" +} + +export const updateStreamer = { + name: "updateStreamer", + text: "INSERT INTO streamer (userid,tokenPair) VALUES ($1,$2)\ + ON CONFLICT (userid) DO UPDATE SET tokenPair = $2" +} + +// Request-related queries +export const getRequests = { + name: "getRequests", + text: "SELECT * FROM get_requests() LIMIT $1" +} + +export const getRequestsVoted = { + name: "getRequestsVoted", + text: "SELECT * FROM get_requests_voted($2) LIMIT $1" +} + +export const getAllRequests = { + name: "getAllRequests", + text: "SELECT * FROM get_requests_all() LIMIT $1" +} + +export const getAllRequestsVoted = { + name: "getAllRequestsVoted", + text: "SELECT * FROM get_requests_all_voted($2) LIMIT $1" +} + +export const checkRequestExists = { + name: "checkRequestExists", + text: "SELECT * FROM requests WHERE url = $1" +} + +export const addRequest = { + name: "addRequest", + text: "CALL add_request($1,$2)" +} + +export const checkValidState = { + name: "checkValidState", + text: "SELECT * FROM states WHERE state = $1" +} + +export const updateRequestState = { + name: "updateRequestState", + text: "UPDATE requests SET state = $2 WHERE url = $1" +} + +export const updateRequestScore = { + name: "updateRequestScore", + text: "UPDATE requests SET score = score + $2 WHERE url = $1" +} + +export const deleteRequest = { + name: "deleteRequest", + text: "DELETE FROM requests WHERE url = $1" +} + +export const checkVoteExists = { + name: "checkVoteExists", + text: "SELECT * FROM votes WHERE requesturl = $1 AND userid = $2" +} diff --git a/src/requests.ts b/src/requests.ts index b3421b2..bb5d834 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -1,69 +1,35 @@ +import * as queries from "./queries" import pg from "pg"; import db from "./db"; -// getRequests -const getRequestsQuery = { - name: "getRequests", - text: "SELECT * FROM get_requests() LIMIT $1" -} - export async function getRequests(count: number) { - var query = Object.assign(getRequestsQuery, { values: [count] }); + var query = Object.assign(queries.getRequests, { values: [count] }); return db.query(query) .then((result: pg.QueryResult) => result.rows); }; -// getAllRequests -const getAllRequestsQuery = { - name: "getAllRequests", - text: "SELECT * FROM get_requests_all() LIMIT $1" -} - export async function getAllRequests(count: number) { - var query = Object.assign(getAllRequestsQuery, { values: [count] }); + var query = Object.assign(queries.getAllRequests, { values: [count] }); return db.query(query) .then((result: pg.QueryResult) => result.rows); }; -// getRequestsVoted -const getRequestsVotedQuery = { - name: "getRequestsVoted", - text: "SELECT * FROM get_requests_voted($2) LIMIT $1" -} - export async function getRequestsVoted(count: number, user: number) { - var query = Object.assign(getRequestsVotedQuery, { values: [count,user] }); + var query = Object.assign(queries.getRequestsVoted, { values: [count,user] }); return db.query(query) .then((result: pg.QueryResult) => result.rows); }; -// getAllRequestsVoted -const getAllRequestsVotedQuery = { - name: "getAllRequestsVoted", - text: "SELECT * FROM get_requests_all_voted($2) LIMIT $1" -} - export async function getAllRequestsVoted(count: number,user: number) { - var query = Object.assign(getAllRequestsVotedQuery, { values: [count,user] }); + var query = Object.assign(queries.getAllRequestsVoted, { values: [count,user] }); return db.query(query) .then((result: pg.QueryResult) => result.rows); }; -// addRequest const validUrlRegexes = [ /^https:\/\/www\.youtube\.com\/watch\?v=[a-zA-Z0-9_-]{11}$/ ]; -const checkRequestExistsQuery = { - name: "checkRequestExists", - text: "SELECT * FROM requests WHERE url = $1" -} - -const addRequestQuery = { - name: "addRequest", - text: "CALL add_request($1,$2)" -} - export async function addRequest(url: string, requester: string): Promise<[number,string]> { var validUrl = false; for (var regex of validUrlRegexes) { @@ -73,70 +39,42 @@ export async function addRequest(url: string, requester: string): Promise<[numbe } } if (!validUrl) return [400, "Invalid song URL."]; - var query = Object.assign(checkRequestExistsQuery, { values: [url] }); + var query = Object.assign(queries.checkRequestExists, { values: [url] }); var result = await db.query(query); if (result.rowCount > 0) { return [200,`Song already requested by ${result.rows[0].requester}. State: ${result.rows[0].state}`] } - var query = Object.assign(addRequestQuery, { values: [url,requester] }); + var query = Object.assign(queries.addRequest, { values: [url,requester] }); return db.query(query) .then(() => [201,"Song request added."] as [number,string]); }; -// updateRequestState -const checkValidStateQuery = { - name: "checkValidState", - text: "SELECT * FROM states WHERE state = $1" -} - -const updateRequestStateQuery = { - name: "updateRequestState", - text: "UPDATE requests SET state = $2 WHERE url = $1" -} - export async function updateRequestState(url: string, state: string): Promise<[number,string]> { - var query = Object.assign(checkValidStateQuery, { values: [state] }); + var query = Object.assign(queries.checkValidState, { values: [state] }); var result = await db.query(query); if (result.rowCount < 1) { return [400,"Invalid state"] } - var query = Object.assign(updateRequestStateQuery, { values: [url,state] }); + var query = Object.assign(queries.updateRequestState, { values: [url,state] }); return db.query(query) .then(() => [200,"Song request state updated."] as [number,string]); }; -// updateRequestScore -const updateRequestScoreQuery = { - name: "updateRequestScore", - text: "UPDATE requests SET score = score + $2 WHERE url = $1" -} - export async function updateRequestScore(url: string, scoreDiff: number): Promise<[number,string]> { - var query = Object.assign(updateRequestScoreQuery, { values: [url,scoreDiff] }); + var query = Object.assign(queries.updateRequestScore, { values: [url,scoreDiff] }); return db.query(query) .then(() => [200,"Song request score updated."] as [number,string]); }; -// deleteRequest -const deleteRequestQuery = { - name: "deleteRequest", - text: "DELETE FROM requests WHERE url = $1" -} - export async function deleteRequest(url: string): Promise<[number,string]> { - var query = Object.assign(deleteRequestQuery, { values: [url] }); + var query = Object.assign(queries.deleteRequest, { values: [url] }); return db.query(query) .then(() => [200,"Song request deleted."] as [number,string]); }; -const checkVoteExistsQuery = { - name: "checkVoteExists", - text: "SELECT * FROM votes WHERE requesturl = $1 AND userid = $2" -} - export async function addVote(url: string, user: string): Promise<[number,string]> { - var query = Object.assign(checkVoteExistsQuery, { values: [url,user] }); + var query = Object.assign(queries.checkVoteExists, { values: [url,user] }); var result = await db.query(query); if (result.rowCount > 0) { return [200,`Song already voted on`]