From 4cce05f7d635a4ea4016fde85fabb81b2c7e11cf Mon Sep 17 00:00:00 2001 From: Ngatia Gichuru Date: Thu, 17 Oct 2024 18:57:05 -0400 Subject: [PATCH] Add initial project structure with Docker setup and frontend/backend files --- backend/Dockerfile | 9 ++ backend/app.py | 145 +++++++++++++++++++++++++++ backend/requirements.txt | 9 ++ db/init.sql | 50 +++++++++ docker-compose.yaml | 41 ++++++++ frontend/Dockerfile | 9 ++ frontend/app.py | 101 +++++++++++++++++++ frontend/requirements.txt | 4 + frontend/templates/dashboard.html | 51 ++++++++++ frontend/templates/eula.html | 13 +++ frontend/templates/index.html | 18 ++++ frontend/templates/login.html | 26 +++++ frontend/templates/register.html | 50 +++++++++ frontend/templates/static/styles.css | 40 ++++++++ frontend/templates/styles.css | 40 ++++++++ 15 files changed, 606 insertions(+) create mode 100755 backend/Dockerfile create mode 100755 backend/app.py create mode 100755 backend/requirements.txt create mode 100755 db/init.sql create mode 100755 docker-compose.yaml create mode 100755 frontend/Dockerfile create mode 100755 frontend/app.py create mode 100755 frontend/requirements.txt create mode 100755 frontend/templates/dashboard.html create mode 100755 frontend/templates/eula.html create mode 100755 frontend/templates/index.html create mode 100755 frontend/templates/login.html create mode 100755 frontend/templates/register.html create mode 100755 frontend/templates/static/styles.css create mode 100755 frontend/templates/styles.css diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100755 index 0000000..6f881af --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.9-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +ENV DATABASE_URL=postgresql://username:password@db/workout_buddy +ENV JWT_SECRET_KEY=super-secret +EXPOSE 5000 +CMD ["python", "app.py"] diff --git a/backend/app.py b/backend/app.py new file mode 100755 index 0000000..fbf1219 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,145 @@ +import os +from flask import Flask, request, jsonify +from flask_sqlalchemy import SQLAlchemy +from flask_restful import Api, Resource +from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity +from werkzeug.security import generate_password_hash, check_password_hash +from marshmallow import Schema, fields, validate +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address +import logging +from logging.handlers import RotatingFileHandler +from flask_cors import CORS + + +app = Flask(__name__) +CORS(app) +logging.basicConfig(level=logging.DEBUG) + +# PostgreSQL database configuration +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['JWT_SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY', 'super-secret') # Change this in production! +app.config['JWT_ACCESS_TOKEN_EXPIRES'] = 3600 # 1 hour +app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'postgresql://workout_user:workout_password@db/workout_buddy') + +db = SQLAlchemy(app) +api = Api(app) +jwt = JWTManager(app) +limiter = Limiter(app, key_func=get_remote_address) + +# Set up logging +if not app.debug: + if not os.path.exists('logs'): + os.mkdir('logs') + file_handler = RotatingFileHandler('logs/workout_buddy.log', maxBytes=10240, backupCount=10) + file_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) + file_handler.setLevel(logging.INFO) + app.logger.addHandler(file_handler) + app.logger.setLevel(logging.INFO) + app.logger.info('Workout Buddy startup') + +class User(db.Model): + __tablename__ = 'users' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + age = db.Column(db.Integer, nullable=False) + location = db.Column(db.String(100), nullable=False) + gender = db.Column(db.String(10), nullable=False) + password_hash = db.Column(db.String(128)) + created_at = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + updated_at = db.Column(db.DateTime(timezone=True), server_default=db.func.now(), onupdate=db.func.now()) + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + +class Message(db.Model): + __tablename__ = 'messages' + id = db.Column(db.Integer, primary_key=True) + sender_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + receiver_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + content = db.Column(db.String(500), nullable=False) + created_at = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + + sender = db.relationship('User', foreign_keys=[sender_id], backref=db.backref('sent_messages', lazy=True)) + receiver = db.relationship('User', foreign_keys=[receiver_id], backref=db.backref('received_messages', lazy=True)) + +class UserSchema(Schema): + name = fields.Str(required=True, validate=validate.Length(min=1, max=100)) + email = fields.Email(required=True) + age = fields.Int(required=True, validate=validate.Range(min=18, max=120)) + location = fields.Str(required=True, validate=validate.Length(min=1, max=100)) + password = fields.Str(required=True, validate=validate.Length(min=8)) + +class MessageSchema(Schema): + content = fields.Str(required=True, validate=validate.Length(min=1, max=500)) + +user_schema = UserSchema() +message_schema = MessageSchema() + +# Define the resource classes +class UserResource(Resource): + def post(self): + try: + data = request.get_json() + errors = user_schema.validate(data) + if errors: + return {'message': 'Validation error', 'errors': errors}, 400 + + existing_user = User.query.filter_by(email=data['email']).first() + if existing_user: + return {'message': 'User with this email already exists'}, 409 + + user = User( + name=data['name'], + email=data['email'], + age=data['age'], + location=data['location'], + gender=data['gender'] + ) + user.set_password(data['password']) + db.session.add(user) + db.session.commit() + return user_schema.dump(user), 201 + except Exception as e: + app.logger.error(f'Error creating user: {str(e)}') + app.logger.info(f'Attempting to create user: {data["email"]}') + return {'message': 'An error occurred while creating the user'}, 500 +class MessageResource(Resource): + @jwt_required() + def get(self): + messages = Message.query.all() + return message_schema.dump(messages, many=True), 200 + + @jwt_required() + def post(self): + data = request.get_json() + message = Message( + sender_id=get_jwt_identity(), # Assuming JWT identity is user ID + receiver_id=data['receiver_id'], + content=data['content'] + ) + db.session.add(message) + db.session.commit() + return message_schema.dump(message), 201 + +class AuthResource(Resource): + def post(self): + data = request.get_json() + user = User.query.filter_by(email=data['email']).first() + if user and user.check_password(data['password']): + access_token = create_access_token(identity=user.id) + return {'access_token': access_token}, 200 + return {'message': 'Invalid credentials'}, 401 + +# Add resource endpoints +api.add_resource(UserResource, '/users') +api.add_resource(MessageResource, '/messages') +api.add_resource(AuthResource, '/auth') + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100755 index 0000000..bd27df5 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,9 @@ +Flask==2.2.5 +Flask-SQLAlchemy==3.0.4 +Flask-RESTful==0.3.9 +Flask-JWT-Extended==4.4.4 +Werkzeug==2.3.0 +marshmallow==3.19.0 +Flask-Limiter==2.7.0 +psycopg2-binary==2.9.7 +flask-cors==3.0.10 diff --git a/db/init.sql b/db/init.sql new file mode 100755 index 0000000..8c0809f --- /dev/null +++ b/db/init.sql @@ -0,0 +1,50 @@ +-- Create Users table +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(120) UNIQUE NOT NULL, + age INTEGER NOT NULL, + location VARCHAR(100) NOT NULL, + gender VARCHAR(10) NOT NULL, + password_hash VARCHAR(128) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Create Messages table +CREATE TABLE IF NOT EXISTS messages ( + id SERIAL PRIMARY KEY, + sender_id INTEGER NOT NULL REFERENCES users(id), + receiver_id INTEGER NOT NULL REFERENCES users(id), + content VARCHAR(500) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Create index for faster queries on location +CREATE INDEX IF NOT EXISTS idx_users_location ON users(location); + +-- Create indexes for faster message retrieval +CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id); +CREATE INDEX IF NOT EXISTS idx_messages_receiver ON messages(receiver_id); + +-- Function to update the updated_at column +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Drop the trigger if it already exists to avoid conflicts +DROP TRIGGER IF EXISTS update_users_updated_at ON users; + +-- Create trigger to automatically update the updated_at column +CREATE TRIGGER update_users_updated_at + BEFORE UPDATE ON users + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- Grant necessary permissions to workout_user +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO workout_user; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO workout_user; diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100755 index 0000000..4429237 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,41 @@ +version: '3.8' + +services: + db: + image: postgres:13 + environment: + - POSTGRES_DB=workout_buddy + - POSTGRES_USER=workout_user + - POSTGRES_PASSWORD=workout_password + volumes: + - db-data:/var/lib/postgresql/data + - ./db/:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD-SHELL", "pg_isready -U workout_user -d workout_buddy"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: ./backend + ports: + - "5000:5000" + environment: + - DATABASE_URL=postgresql://workout_user:workout_password@db/workout_buddy + - JWT_SECRET_KEY=super-secret + depends_on: + db: + condition: service_healthy + + frontend: + build: ./frontend + ports: + - "8000:8000" + environment: + - SECRET_KEY=super-secret + - API_URL=http://backend:5000 + depends_on: + - backend +# +volumes: + db-data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100755 index 0000000..fa045d1 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.9-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +ENV SECRET_KEY=super-secret +ENV API_URL=http://backend:5000 +EXPOSE 8000 +CMD ["python", "app.py"] \ No newline at end of file diff --git a/frontend/app.py b/frontend/app.py new file mode 100755 index 0000000..248071a --- /dev/null +++ b/frontend/app.py @@ -0,0 +1,101 @@ + +import os +from flask import Flask, render_template, redirect, url_for, request, session, jsonify +import requests + +app = Flask(__name__) +app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'super-secret') + +API_URL = os.environ.get('API_URL', 'http://localhost:5000') + +@app.route('/') +def index(): + if 'access_token' in session: + return redirect(url_for('dashboard')) + return render_template('index.html') + +@app.route('/register', methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + data = { + 'name': request.form['name'], + 'email': request.form['email'], + 'age': request.form['age'], + 'location': request.form['location'], + 'gender': request.form['gender'], + 'password': request.form['password'] + } + response = requests.post(f"{API_URL}/users", json=data) + if response.status_code == 201: + return redirect(url_for('login')) + else: + return render_template('register.html', error=response.json().get('message', 'Error during registration')) + return render_template('register.html') + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + email = request.form['email'] + password = request.form['password'] + response = requests.post(f"{API_URL}/auth", json={'email': email, 'password': password}) + if response.status_code == 200: + session['access_token'] = response.json()['access_token'] + # Save the user ID from the access token or fetch it from the API + session['user_id'] = email # Or any unique identifier like user ID + return redirect(url_for('dashboard')) + else: + return render_template('login.html', error=response.json().get('message', 'Invalid credentials')) + return render_template('login.html') + +@app.route('/dashboard') +def dashboard(): + if 'access_token' not in session: + return redirect(url_for('login')) + + # Fetch the current user's details + headers = {'Authorization': f'Bearer {session["access_token"]}'} + user_response = requests.get(f"{API_URL}/users/me", headers=headers) # Adjust the API URL as necessary + user = user_response.json() if user_response.status_code == 200 else None + + # Handle search filters + location = request.args.get('location', '') + age = request.args.get('age', '') + gender = request.args.get('gender', 'Any') + + filters = {} + if location: + filters['location'] = location + if age: + filters['age'] = age + if gender and gender != "Any": + filters['gender'] = gender + + # Fetch users based on filters + response = requests.get(f"{API_URL}/users", params=filters, headers=headers) + users = response.json() if response.status_code == 200 else [] + + # Fetch messages for the current user + messages_response = requests.get(f"{API_URL}/messages", headers=headers) + messages = messages_response.json() if messages_response.status_code == 200 else [] + + return render_template('dashboard.html', user=user, users=users, messages=messages) + +@app.route('/messages', methods=['POST']) +def send_message(): + if 'access_token' not in session: + return redirect(url_for('login')) + + data = { + 'receiver_id': request.form['receiver_id'], + 'content': request.form['message_content'] + } + headers = {'Authorization': f'Bearer {session["access_token"]}', 'Content-Type': 'application/json'} + response = requests.post(f"{API_URL}/messages", headers=headers, json=data) + + if response.status_code == 201: + return redirect(url_for('dashboard')) + else: + return jsonify({'message': 'Error sending message'}), 500 + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8000, debug=True) diff --git a/frontend/requirements.txt b/frontend/requirements.txt new file mode 100755 index 0000000..13576ea --- /dev/null +++ b/frontend/requirements.txt @@ -0,0 +1,4 @@ +Flask==2.2.5 +requests==2.31.0 +Werkzeug==2.3.0 +Flask-JWT-Extended==4.4.4 diff --git a/frontend/templates/dashboard.html b/frontend/templates/dashboard.html new file mode 100755 index 0000000..f9a931c --- /dev/null +++ b/frontend/templates/dashboard.html @@ -0,0 +1,51 @@ + + + + + + Dashboard + + + +
+

Welcome, {{ user.name }}

+ Logout + +

Search Users

+
+ + + + +
+ +

Users

+ + +

Messages

+
+ + + +
+ + +
+ + diff --git a/frontend/templates/eula.html b/frontend/templates/eula.html new file mode 100755 index 0000000..ba726f2 --- /dev/null +++ b/frontend/templates/eula.html @@ -0,0 +1,13 @@ + + + + + + End User License Agreement + + +

End User License Agreement (EULA)

+

[Insert your EULA text here]

+ Back to Registration + + diff --git a/frontend/templates/index.html b/frontend/templates/index.html new file mode 100755 index 0000000..e1374c5 --- /dev/null +++ b/frontend/templates/index.html @@ -0,0 +1,18 @@ + + + + + + Home + + + +
+

Welcome to the Workout Buddy

+
+ Login + Register +
+
+ + diff --git a/frontend/templates/login.html b/frontend/templates/login.html new file mode 100755 index 0000000..6317116 --- /dev/null +++ b/frontend/templates/login.html @@ -0,0 +1,26 @@ + + + + + + Login + + + +
+

Login

+
+ + + + + + + +
+ {% if error %} +

{{ error }}

+ {% endif %} +
+ + diff --git a/frontend/templates/register.html b/frontend/templates/register.html new file mode 100755 index 0000000..657b7b5 --- /dev/null +++ b/frontend/templates/register.html @@ -0,0 +1,50 @@ + + + + + + Register + + + +
+

Register

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+

By registering, you agree to our End User License Agreement (EULA).

+ +
+ {% if error %} +

{{ error }}

+ {% endif %} +
+ + + diff --git a/frontend/templates/static/styles.css b/frontend/templates/static/styles.css new file mode 100755 index 0000000..5ec4f71 --- /dev/null +++ b/frontend/templates/static/styles.css @@ -0,0 +1,40 @@ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; /* Light background color for better visibility */ +} + +.container { + max-width: 400px; /* Limit the width of the form container */ + margin: 50px auto; /* Center the form horizontally and add vertical margin */ + padding: 20px; + background-color: white; /* White background for the form */ + border-radius: 8px; /* Rounded corners for the form */ + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */ +} + +h1 { + text-align: center; /* Center the heading */ +} + +.form-group { + margin-bottom: 500px; /* Increase space between form groups */ +} + +button { + width: 100%; /* Make the button full-width */ + padding: 10px; /* Add padding to the button */ + background-color: #28a745; /* Green background color */ + color: white; /* White text color */ + border: none; /* Remove default border */ + border-radius: 5px; /* Rounded corners for the button */ + cursor: pointer; /* Change cursor to pointer on hover */ +} + +button:hover { + background-color: #218838; /* Darker green on hover */ +} + +.error { + color: red; /* Red color for error messages */ + text-align: center; /* Center error messages */ +} diff --git a/frontend/templates/styles.css b/frontend/templates/styles.css new file mode 100755 index 0000000..5ec4f71 --- /dev/null +++ b/frontend/templates/styles.css @@ -0,0 +1,40 @@ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; /* Light background color for better visibility */ +} + +.container { + max-width: 400px; /* Limit the width of the form container */ + margin: 50px auto; /* Center the form horizontally and add vertical margin */ + padding: 20px; + background-color: white; /* White background for the form */ + border-radius: 8px; /* Rounded corners for the form */ + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */ +} + +h1 { + text-align: center; /* Center the heading */ +} + +.form-group { + margin-bottom: 500px; /* Increase space between form groups */ +} + +button { + width: 100%; /* Make the button full-width */ + padding: 10px; /* Add padding to the button */ + background-color: #28a745; /* Green background color */ + color: white; /* White text color */ + border: none; /* Remove default border */ + border-radius: 5px; /* Rounded corners for the button */ + cursor: pointer; /* Change cursor to pointer on hover */ +} + +button:hover { + background-color: #218838; /* Darker green on hover */ +} + +.error { + color: red; /* Red color for error messages */ + text-align: center; /* Center error messages */ +}