Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 058f3ae702 | |||
| be6ab1f929 | |||
| 3165bf1aa9 | |||
| 29a4e5bb22 | |||
| dc919681fd | |||
| 0589b2c8a5 | |||
| 96a5bcc9e8 | |||
| 66f1b26cc8 |
52
db/database.py
Normal file
52
db/database.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import os
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
|
||||
class PostgresConnector:
|
||||
"""
|
||||
Simple PostgreSQL connector (single connection).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.connection = psycopg2.connect(
|
||||
host=os.getenv("POSTGRES_HOST", "localhost"),
|
||||
port=os.getenv("POSTGRES_PORT", 5432),
|
||||
user=os.getenv("POSTGRES_USER", "postgres"),
|
||||
password=os.getenv("POSTGRES_PASSWORD", "postgres"),
|
||||
database=os.getenv("POSTGRES_DB", "postgres"),
|
||||
)
|
||||
self.connection.autocommit = False
|
||||
|
||||
def execute(self, query, params=None, fetch=False):
|
||||
with self.connection.cursor(cursor_factory=RealDictCursor) as cursor:
|
||||
cursor.execute(query, params)
|
||||
if fetch:
|
||||
return cursor.fetchall()
|
||||
self.connection.commit()
|
||||
|
||||
def executemany(self, query, param_list):
|
||||
with self.connection.cursor(cursor_factory=RealDictCursor) as cursor:
|
||||
cursor.executemany(query, param_list)
|
||||
self.connection.commit()
|
||||
|
||||
def save_user(self, username, email, password_hash):
|
||||
query = """
|
||||
INSERT INTO users (username, email, password_hash)
|
||||
VALUES (%s, %s, %s)
|
||||
"""
|
||||
self.execute(query, (username, email, password_hash))
|
||||
|
||||
def get_user_by_username(self, username) -> dict:
|
||||
query = "SELECT id, username, email, password_hash FROM users WHERE username = %s"
|
||||
result = self.execute(query, (username,), fetch=True)
|
||||
return result[0] if result else None
|
||||
|
||||
def get_user_by_email(self, email) -> dict:
|
||||
query = "SELECT id, username, email, password_hash FROM users WHERE email = %s"
|
||||
result = self.execute(query, (email,), fetch=True)
|
||||
return result[0] if result else None
|
||||
|
||||
def close(self):
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
35
db/schema.sql
Normal file
35
db/schema.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE has_access (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
post_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE posts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
author VARCHAR(255) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
source VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE comments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
post_id INTEGER NOT NULL,
|
||||
author VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
reply_to VARCHAR(255),
|
||||
source VARCHAR(255) NOT NULL,
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
|
||||
);
|
||||
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
container_name: postgres_db
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- ./db/postgres_vol:/var/lib/postgresql/data
|
||||
- ./db/schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -1,10 +1,12 @@
|
||||
beautifulsoup4==4.14.3
|
||||
Flask==3.1.2
|
||||
Flask==3.1.3
|
||||
flask_cors==6.0.2
|
||||
google_api_python_client==2.188.0
|
||||
keybert==0.9.0
|
||||
nltk==3.9.2
|
||||
pandas==3.0.0
|
||||
numpy==2.4.2
|
||||
pandas==3.0.1
|
||||
psycopg2==2.9.11
|
||||
psycopg2_binary==2.9.11
|
||||
python-dotenv==1.2.1
|
||||
Requests==2.32.5
|
||||
sentence_transformers==5.2.2
|
||||
|
||||
@@ -1,21 +1,102 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from flask import Flask, jsonify, request
|
||||
from flask_cors import CORS
|
||||
from flask_bcrypt import Bcrypt
|
||||
from flask_jwt_extended import (
|
||||
JWTManager,
|
||||
create_access_token,
|
||||
jwt_required,
|
||||
get_jwt_identity
|
||||
)
|
||||
|
||||
from server.stat_gen import StatGen
|
||||
from db.database import PostgresConnector
|
||||
from server.auth import AuthManager
|
||||
|
||||
import pandas as pd
|
||||
import traceback
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
db = PostgresConnector()
|
||||
|
||||
# Allow for CORS from localhost:5173
|
||||
CORS(app, resources={r"/*": {"origins": "http://localhost:5173"}})
|
||||
# Env Variables
|
||||
load_dotenv()
|
||||
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
||||
jwt_secret_key = os.getenv("JWT_SECRET_KEY", "super-secret-change-this")
|
||||
jwt_access_token_expires = int(os.getenv("JWT_ACCESS_TOKEN_EXPIRES", 1200)) # Default to 20 minutes
|
||||
|
||||
# Flask Configuration
|
||||
CORS(app, resources={r"/*": {"origins": frontend_url}})
|
||||
app.config["JWT_SECRET_KEY"] = jwt_secret_key
|
||||
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = jwt_access_token_expires
|
||||
|
||||
bcrypt = Bcrypt(app)
|
||||
jwt = JWTManager(app)
|
||||
auth_manager = AuthManager(db, bcrypt)
|
||||
|
||||
# Global State
|
||||
posts_df = pd.read_json('small.jsonl', lines=True)
|
||||
with open("topic_buckets.json", "r", encoding="utf-8") as f:
|
||||
domain_topics = json.load(f)
|
||||
stat_obj = StatGen(posts_df, domain_topics)
|
||||
# posts_df = pd.read_json('small.jsonl', lines=True)
|
||||
# with open("topic_buckets.json", "r", encoding="utf-8") as f:
|
||||
# domain_topics = json.load(f)
|
||||
# stat_obj = StatGen(posts_df, domain_topics)
|
||||
stat_obj = None
|
||||
|
||||
@app.route('/register', methods=['POST'])
|
||||
def register_user():
|
||||
data = request.get_json()
|
||||
|
||||
if not data or "username" not in data or "email" not in data or "password" not in data:
|
||||
return jsonify({"error": "Missing username, email, or password"}), 400
|
||||
|
||||
username = data["username"]
|
||||
email = data["email"]
|
||||
password = data["password"]
|
||||
|
||||
try:
|
||||
auth_manager.register_user(username, email, password)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
print(traceback.format_exc())
|
||||
return jsonify({"error": f"An unexpected error occurred: {str(e)}"}), 500
|
||||
|
||||
print(f"Registered new user: {username}")
|
||||
return jsonify({"message": f"User '{username}' registered successfully"}), 200
|
||||
|
||||
@app.route('/login', methods=['POST'])
|
||||
def login_user():
|
||||
data = request.get_json()
|
||||
|
||||
if not data or "username" not in data or "password" not in data:
|
||||
return jsonify({"error": "Missing username or password"}), 400
|
||||
|
||||
username = data["username"]
|
||||
password = data["password"]
|
||||
|
||||
try:
|
||||
user = auth_manager.authenticate_user(username, password)
|
||||
if user:
|
||||
access_token = create_access_token(identity=str(user['id']))
|
||||
return jsonify({"access_token": access_token}), 200
|
||||
else:
|
||||
return jsonify({"error": "Invalid username or password"}), 401
|
||||
except Exception as e:
|
||||
print(traceback.format_exc())
|
||||
return jsonify({"error": f"An unexpected error occurred: {str(e)}"}), 500
|
||||
|
||||
@app.route("/profile", methods=["GET"])
|
||||
@jwt_required()
|
||||
def profile():
|
||||
current_user = get_jwt_identity()
|
||||
|
||||
return jsonify(
|
||||
message="Access granted",
|
||||
user=auth_manager.get_user_by_id(current_user)
|
||||
), 200
|
||||
|
||||
|
||||
@app.route('/upload', methods=['POST'])
|
||||
def upload_data():
|
||||
|
||||
29
server/auth.py
Normal file
29
server/auth.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from db.database import PostgresConnector
|
||||
from flask_bcrypt import Bcrypt
|
||||
|
||||
class AuthManager:
|
||||
def __init__(self, db: PostgresConnector, bcrypt: Bcrypt):
|
||||
self.db = db
|
||||
self.bcrypt = bcrypt
|
||||
|
||||
def register_user(self, username, email, password):
|
||||
hashed_password = self.bcrypt.generate_password_hash(password).decode("utf-8")
|
||||
|
||||
if self.db.get_user_by_email(email):
|
||||
raise ValueError("Email already registered")
|
||||
|
||||
if self.db.get_user_by_username(username):
|
||||
raise ValueError("Username already taken")
|
||||
|
||||
self.db.save_user(username, email, hashed_password)
|
||||
|
||||
def authenticate_user(self, username, password):
|
||||
user = self.db.get_user_by_username(username)
|
||||
if user and self.bcrypt.check_password_hash(user['password_hash'], password):
|
||||
return user
|
||||
return None
|
||||
|
||||
def get_user_by_id(self, user_id):
|
||||
query = "SELECT id, username, email FROM users WHERE id = %s"
|
||||
result = self.db.execute(query, (user_id,), fetch=True)
|
||||
return result[0] if result else None
|
||||
Reference in New Issue
Block a user