diff options
| author | Pinapelz <donaldshan1@outlook.com> | 2023-09-18 17:41:59 -0700 |
|---|---|---|
| committer | Pinapelz <donaldshan1@outlook.com> | 2023-09-18 17:41:59 -0700 |
| commit | c2239d5e360d8d44a18e76c562d03a01dea05c74 (patch) | |
| tree | 898360217353744ade79750bb1dc6be16070c227 /api | |
Initial Commit
Diffstat (limited to 'api')
| -rw-r--r-- | api/app.py | 221 | ||||
| -rw-r--r-- | api/static/index.css | 113 | ||||
| -rw-r--r-- | api/static/index.js | 114 | ||||
| -rw-r--r-- | api/static/server_auth.js | 93 | ||||
| -rw-r--r-- | api/templates/index.html | 38 | ||||
| -rw-r--r-- | api/templates/server_auth.html | 22 |
6 files changed, 601 insertions, 0 deletions
diff --git a/api/app.py b/api/app.py new file mode 100644 index 0000000..e16acac --- /dev/null +++ b/api/app.py @@ -0,0 +1,221 @@ +from flask import Flask, render_template, jsonify, request +import configparser +import psycopg2 +from psycopg2 import Error +import os +import secrets +import string + +app = Flask(__name__) + +class PostgresHandler: + def __init__(self, username: str, password: str, host_name: str, port: int, database: str): + db_params = { + "dbname": database, + "user": username, + "password": password, + "host": host_name, + "port": port + } + self._connection = psycopg2.connect(**db_params) + print("Handler Success") + + + def create_table(self, name: str, column: str): + cursor = self._connection.cursor() + cursor.execute(f"CREATE TABLE IF NOT EXISTS {name} ({column})") + self._connection.commit() + cursor.close() + + def check_row_exists(self, table_name: str, column_name: str, value: str): + cursor = self._connection.cursor() + query = f"SELECT 1 FROM {table_name} WHERE {column_name} = %s" + cursor.execute(query, (value,)) + result = cursor.fetchone() + cursor.close() + + if result is not None: + return True + else: + return False + + def insert_row(self, table_name, column, data): + try: + cursor = self._connection.cursor() + placeholders = ', '.join(['%s'] * len(data)) + query = f"INSERT INTO {table_name} ({column}) VALUES ({placeholders})" + cursor.execute(query, data) + self._connection.commit() + print("Data Inserted:", data) + except Error as err: + self._connection.rollback() + print("Error inserting data") + print(err) + if "duplicate key" not in str(err).lower(): + return False + return True + + def get_rows(self, table_name: str, column: str, value: str): + try: + cursor = self._connection.cursor() + query = f"SELECT * FROM {table_name} WHERE {column} = %s" + cursor.execute(query, (value,)) + result = cursor.fetchall() + return result + except Error as e: + self._connection.rollback() + print(f"Failed to fetch row from {table_name} WHERE {column} is {value}") + print(e) + return False + + def get_random_row(self, table_name: str, count: int, condition: str = None): + if condition is None: + condition = "1 = 1" + try: + cursor = self._connection.cursor() + query = f"SELECT * FROM {table_name} WHERE {condition} ORDER BY RANDOM() LIMIT {str(count)}" + cursor.execute(query) + result = cursor.fetchall() + return result + except Error as e: + self._connection.rollback() + print(f"Failed to select random rows from {table_name}") + print(e) + return False + + def check_health(self): + cursor = self._connection.cursor() + cursor.execute("SELECT 1") + result = cursor.fetchone() + cursor.close() + if result is not None: + return True + else: + return False + + def delete_row(self, table_name: str, column: str, value: str): + try: + cursor = self._connection.cursor() + query = f"DELETE FROM {table_name} WHERE {column} = %s" + cursor.execute(query, (value,)) + self._connection.commit() + print("Data Deleted:", value) + except Error as e: + self._connection.rollback() + print(f"Failed to delete row from {table_name} WHERE {column} is {value}") + print(e) + return False + return True + + + def close_connection(self): + self._connection.close() + + +parser = configparser.ConfigParser() +parser.read("config.ini") +CONFIG = parser + +def create_database_connection(auth_append: str = ""): + """ + Creates a database connection using the environment variables + If not available use the config.ini file + :param: auth_append: str = "" - If you want to use a different set of variables for persisitance of sessions + + auth_append will be specified in usage when waiting for user's answers, this DB will track/verify answers + """ + if os.environ.get(auth_append+"DB_HOSTNAME") is not None: + hostname = os.environ.get(auth_append+"DB_HOSTNAME") + user = os.environ.get(auth_append+"DB_USER") + password = os.environ.get(auth_append+"DB_PASSWORD") + database = os.environ.get(auth_append+"DB_DATABASE") + else: + hostname = CONFIG.get(auth_append+"database", "host") + user = CONFIG.get(auth_append+"database", "user") + password = CONFIG.get(auth_append+"database", "password") + database = CONFIG.get(auth_append+"database", "database") + return PostgresHandler(host_name=hostname, username=user, password=password, database=database, port=5432) + +def initialize_auth_database(): + server = create_database_connection("AUTH_") + server.create_table("sessions", "session_id VARCHAR(255) PRIMARY KEY, answer VARCHAR(1000), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP") + server.close_connection() +initialize_auth_database() + +@app.route('/') +def index_demo(): + return render_template('index.html') + +@app.route('/server_auth') +def server_side_auth_demo(): + return render_template('server_auth.html') + +@app.route('/api/affiliation/<org>') +def generate_organization_captcha(org): + server = create_database_connection() + create_session = False + if(request.args.get('auth') == "server"): + create_session = True + if server.check_health() is False: + return jsonify({"error": "Database Connection Failed. Dynamic Affiliation Endpoint requires a PostgreSQL Connection"}), 500 + if server.check_row_exists("vtuber_data", "organization", org) is False: + return jsonify({"error": "Organization not found in Database"}), 404 + correct_answers= server.get_random_row('vtuber_data', 5, "organization = '"+org+"'") + random_answers = server.get_random_row('vtuber_data', 11) + server.close_connection() + question_data = [{"image": question[3], "name": question[1], "affiliation": question[2], "id": question[0] } for question in correct_answers + random_answers] + if create_session: + server = create_database_connection("AUTH_") + session_id = secrets.token_urlsafe(16) + solutions = [] + for question in question_data: + if question['affiliation'] == org: + solutions.append(question['id']) + server.insert_row("sessions", "session_id, answer", (session_id, ",".join(solutions))) + return_data = { + "category": "affiliation", + "title": "Select all the VTuber affiliated with "+org, + "questions": question_data, + "onFail": { + "text": "You got some wrong", + "extra": None + }, + "session": session_id + } + else: + for question in question_data: + if question['affiliation'] == org: + question['answer'] = True + else: + question['answer'] = False + return_data = { + "category": "affiliation", + "title": "Select all the VTuber affiliated with "+org, + "questions": question_data, + "onFail": { + "text": "You got some wrong", + "extra": None + } + } + return jsonify(return_data) + +@app.route("/api/verify", methods=["POST"]) +def verify_answers(): + session_id = request.form.get('session') + answer = request.form.get('answer') + server = create_database_connection("AUTH_") + if server.check_health() is False: + return jsonify({"error": "Cannot connect to verification database"}), 500 + if server.check_row_exists("sessions", "session_id", session_id) is False: + return jsonify({"error": "Session ID not found"}), 404 + correct_answers = server.get_rows("sessions", "session_id", session_id)[0][1].split(",") + server.delete_row("sessions", "session_id", session_id) + server.close_connection() + if answer == ",".join(correct_answers): + return jsonify({"success": True}) + else: + return jsonify({"success": False}) + + +if __name__ == "__main__": + app.run(debug=True)
\ No newline at end of file diff --git a/api/static/index.css b/api/static/index.css new file mode 100644 index 0000000..6499589 --- /dev/null +++ b/api/static/index.css @@ -0,0 +1,113 @@ +#recaptcha-container { + border-radius: 5px; + border: 1px solid #ccc; + padding: 20px; + width: 440px; + margin: 50px auto; + background-color: #f4f4f4; + } + + .recaptcha-header { + background-color: #0079c1; + color: #fff; + padding: 10px 15px; + border-radius: 5px; + font-weight: bold; + margin-bottom: 20px; + text-align: center; + } + + .recaptcha-row { + display: flex; + justify-content: space-between; + margin-bottom: 5px; + } + + .submit-btn { + display: block; + margin: 10px auto 20px; + /* This will center the button horizontally and give it a margin at the bottom */ + padding: 10px 15px; + background-color: #0079c1; + color: #fff; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; + } + + .submit-btn:hover { + background-color: #005a91; + } + + .recaptcha-image { + width: 90px; + height: 90px; + margin: 2px; + border: 1px solid transparent; + cursor: pointer; + transition: transform 0.3s ease, border-color 0.3s ease; + background-size: cover; + background-position: center; + /* Ensure the image is centered */ + position: relative; + overflow: hidden; + } + + .regenerate-btn { + display: block; + margin: 10px auto; + /* This will center the button horizontally */ + } + + .recaptcha-image > svg { + display: none; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 30px; + height: 30px; + } + + .selected { + border-color: blue; + transform: scale(0.8); + } + + .selected > svg { + display: block; + } + + #answer-table-container { + margin-top: 20px; + border: 1px solid #ddd; + width: 100%; + max-width: 800px; + margin-left: auto; + margin-right: auto; +} + +#answer-table { + width: 100%; + border-collapse: collapse; +} + +#answer-table th, #answer-table td { + border: 1px solid #ddd; + padding: 8px 12px; + text-align: left; +} + +#answer-table th { + background-color: #f2f2f2; +} + +#answer-table tbody tr:hover { + background-color: #eaeaea; +} + +#answer-table td img { + max-width: 100%; + height: auto; +} diff --git a/api/static/index.js b/api/static/index.js new file mode 100644 index 0000000..1b5e85e --- /dev/null +++ b/api/static/index.js @@ -0,0 +1,114 @@ +let captchaData; + +document.addEventListener("DOMContentLoaded", function () { + fetchCaptchaImages(); +}); + +function toggleSelection(element) { + element.classList.toggle("selected"); +} + +function populateCaptcha(data) { + const container = document.getElementById("recaptcha-container"); + const title = document.getElementById("recaptcha-title"); + title.innerHTML = data.title; + const oldRows = container.getElementsByClassName("recaptcha-row"); + while (oldRows.length > 0) { + oldRows[0].parentNode.removeChild(oldRows[0]); + } + + const images = data.questions; + + let rows = Math.ceil(images.length / 4); + let htmlContent = ""; + for (let r = 0; r < rows; r++) { + htmlContent += '<div class="recaptcha-row">'; + for (let i = 0; i < 4; i++) { + const index = r * 4 + i; + if (images[index]) { + htmlContent += ` + <div class="recaptcha-image" style="background-image: url('${images[index].image}');" onclick="toggleSelection(this)"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <path d="M20.285 2l-1.272.02L6.72 15.466l-5.644-5.58L0 12.352l7.218 7.15L24 2.083z"/> + </svg> + </div> + `; + } + } + htmlContent += "</div>"; + } + + title.insertAdjacentHTML("afterend", htmlContent); + captchaData = data; +} + +async function verifyCaptcha() { + const selectedImages = document.querySelectorAll(".recaptcha-image.selected"); + const answers = []; + const answerTableBody = document.querySelector("#answer-table tbody"); + const endMessageDiv = document.querySelector("#end-message"); + answerTableBody.innerHTML = ""; + + selectedImages.forEach((img) => { + const backgroundImageURL = img.style.backgroundImage; + const imageURL = backgroundImageURL.slice(5, backgroundImageURL.length - 2); + const question = captchaData.questions.find((q) => q.image === imageURL); + answers.push(question.id); + const row = document.createElement("tr"); + const imgCell = document.createElement("td"); + const idCell = document.createElement("td"); + const nameCell = document.createElement("td"); + imgCell.innerHTML = `<img src="${question.image}" alt="Selected Image" width="50">`; + idCell.textContent = question.answer; + nameCell.textContent = question.name; + row.appendChild(imgCell); + row.appendChild(nameCell); + row.appendChild(idCell); + answerTableBody.appendChild(row); + }); + num_correct = 0 + for (let i = 0; i < captchaData.questions.length; i++) { + if (captchaData.questions[i].answer == true) { + num_correct += 1 + } + } + endMessageDiv.textContent = `You selected ${selectedImages.length} image(s). There are ${num_correct} correct image(s).`; + + const captchaAnswer = { + session: captchaData.session, + answer: answers.join(","), + }; + try { + const response = await fetch("/api/verify", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams(captchaAnswer).toString() // Convert JSON object to form-urlencoded data + }); + const data = await response.json(); + + if (data.success) { + endMessageDiv.textContent = "You have successfully completed the captcha!"; + } else { + fetchCaptchaImages(); + } + } catch (error) { + console.error("Error verifying captcha:", error); + } +} + + +const submitButton = document.querySelector(".submit-btn"); +submitButton.addEventListener("click", verifyCaptcha); + +function fetchCaptchaImages() { + fetch("/api/affiliation/Hololive") + .then((response) => response.json()) + .then((data) => { + populateCaptcha(data); + }) + .catch((error) => { + console.error("There was an error fetching the captcha:", error); + }); +}
\ No newline at end of file diff --git a/api/static/server_auth.js b/api/static/server_auth.js new file mode 100644 index 0000000..ead24a7 --- /dev/null +++ b/api/static/server_auth.js @@ -0,0 +1,93 @@ +let captchaData; + +document.addEventListener("DOMContentLoaded", function () { + fetchCaptchaImages(); +}); + +function toggleSelection(element) { + element.classList.toggle("selected"); +} + +function populateCaptcha(data) { + const container = document.getElementById("recaptcha-container"); + const title = document.getElementById("recaptcha-title"); + title.innerHTML = data.title; + const oldRows = container.getElementsByClassName("recaptcha-row"); + while (oldRows.length > 0) { + oldRows[0].parentNode.removeChild(oldRows[0]); + } + + const images = data.questions; + + let rows = Math.ceil(images.length / 4); + let htmlContent = ""; + for (let r = 0; r < rows; r++) { + htmlContent += '<div class="recaptcha-row">'; + for (let i = 0; i < 4; i++) { + const index = r * 4 + i; + if (images[index]) { + htmlContent += ` + <div class="recaptcha-image" style="background-image: url('${images[index].image}');" onclick="toggleSelection(this)"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <path d="M20.285 2l-1.272.02L6.72 15.466l-5.644-5.58L0 12.352l7.218 7.15L24 2.083z"/> + </svg> + </div> + `; + } + } + htmlContent += "</div>"; + } + + title.insertAdjacentHTML("afterend", htmlContent); + captchaData = data; + session_id = data.session_id; +} + +async function verifyCaptcha() { + const selectedImages = document.querySelectorAll(".recaptcha-image.selected"); + const answers = []; + selectedImages.forEach((img) => { + const backgroundImageURL = img.style.backgroundImage; + const imageURL = backgroundImageURL.slice(5, backgroundImageURL.length - 2); + answers.push( + captchaData.questions.find((question) => question.image === imageURL).id + ); + }); + const captchaAnswer = { + session: captchaData.session, + answer: answers.join(","), + }; + try { + const response = await fetch("/api/verify", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams(captchaAnswer).toString(), // Convert JSON object to form-urlencoded data + }); + const data = await response.json(); + + if (data.success) { + alert("Captcha verification successful!"); + } else { + alert("Sorry you got some wrong. Please try again.") + fetchCaptchaImages(); + } + } catch (error) { + console.error("Error verifying captcha:", error); + } +} + +const submitButton = document.querySelector(".submit-btn"); +submitButton.addEventListener("click", verifyCaptcha); + +function fetchCaptchaImages() { + fetch("/api/affiliation/Nijisanji?auth=server") + .then((response) => response.json()) + .then((data) => { + populateCaptcha(data); + }) + .catch((error) => { + console.error("There was an error fetching the captcha:", error); + }); +} diff --git a/api/templates/index.html b/api/templates/index.html new file mode 100644 index 0000000..5bdf1ad --- /dev/null +++ b/api/templates/index.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='index.css') }}"> + <title>reCAPTCHA Image Selection Panel</title> + </head> + + <body> + <div id="recaptcha-container"> + <div class="recaptcha-header" id="recaptcha-title"> + </div> + <button class="regenerate-btn" onclick="fetchCaptchaImages()"> + Regenerate Captcha + </button> + <button class="submit-btn">Submit</button> + </div> + <div id="end-message"> + <!-- Will be populated with JS -->> + </div> + <div id="answer-table-container"> + <table id="answer-table"> + <thead> + <tr> + <th>Image</th> + <th>Name</th> + <th>Answer (ID)</th> + </tr> + </thead> + <tbody> + <!-- Answers will be populated here --> + </tbody> + </table> + </div> + <script src="{{ url_for('static', filename='index.js') }}"></script> + </body> +</html>
\ No newline at end of file diff --git a/api/templates/server_auth.html b/api/templates/server_auth.html new file mode 100644 index 0000000..cf37aa8 --- /dev/null +++ b/api/templates/server_auth.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='index.css') }}"> + <title>Server Side Auth</title> + </head> + + <body> + <div id="recaptcha-container"> + <div class="recaptcha-header" id="recaptcha-title"> + </div> + <button class="regenerate-btn" onclick="fetchCaptchaImages()"> + Regenerate Captcha + </button> + <button class="submit-btn">Submit</button> + </div> + <script src="{{ url_for('static', filename='server_auth.js') }}"></script> + </body> + +</html>
\ No newline at end of file |
