aboutsummaryrefslogtreecommitdiffstats
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/app.py221
-rw-r--r--api/static/index.css113
-rw-r--r--api/static/index.js114
-rw-r--r--api/static/server_auth.js93
-rw-r--r--api/templates/index.html38
-rw-r--r--api/templates/server_auth.html22
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
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage