Google AUTH and Readme
This commit is contained in:
parent
5a900e0ded
commit
bac1d84b09
89
README.md
Normal file
89
README.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# Decision Tree Helpdesk Application
|
||||||
|
|
||||||
|
This application provides a decision tree-based helpdesk system to guide users through troubleshooting steps.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Interactive decision tree navigation
|
||||||
|
- Google OAuth authentication
|
||||||
|
- Admin interface for managing decision trees
|
||||||
|
- User-friendly interface
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Python 3.7+
|
||||||
|
- pip (Python package manager)
|
||||||
|
- A Google Developer account (for OAuth)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```
|
||||||
|
git clone https://github.com/yourusername/decision-tree-helpdesk.git
|
||||||
|
cd decision-tree-helpdesk
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create and activate a virtual environment:
|
||||||
|
```
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
```
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Set up environment variables:
|
||||||
|
Create a `.env` file in the root directory with the following variables:
|
||||||
|
```
|
||||||
|
GOOGLE_CLIENT_ID=your_client_id_here
|
||||||
|
GOOGLE_CLIENT_SECRET=your_client_secret_here
|
||||||
|
SECRET_KEY=your_secret_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Initialize the database:
|
||||||
|
```
|
||||||
|
flask db init
|
||||||
|
flask db migrate -m "Initial migration"
|
||||||
|
flask db upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
1. Start the development server:
|
||||||
|
```
|
||||||
|
flask run
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Open your browser and navigate to:
|
||||||
|
```
|
||||||
|
http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### User Guide
|
||||||
|
|
||||||
|
1. Log in using your Google account
|
||||||
|
2. Navigate through the decision tree by answering questions
|
||||||
|
3. Follow the recommended solutions for your issue
|
||||||
|
|
||||||
|
### Admin Guide
|
||||||
|
|
||||||
|
1. Log in as an administrator
|
||||||
|
2. Access the admin panel to manage decision trees
|
||||||
|
3. Create, edit, or delete nodes in the decision tree
|
||||||
|
4. Define question and answer paths
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch: `git checkout -b feature-name`
|
||||||
|
3. Commit your changes: `git commit -m 'Add some feature'`
|
||||||
|
4. Push to the branch: `git push origin feature-name`
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
26
app.py
26
app.py
@ -1,33 +1,39 @@
|
|||||||
from flask import Flask
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from flask import Flask, render_template
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from models import db, User # Our database instance
|
from models import db, User # Our database instance
|
||||||
from user import user_bp
|
from user import user_bp
|
||||||
from admin import admin_bp
|
from admin import admin_bp
|
||||||
from auth import auth_bp
|
from auth import auth_bp
|
||||||
|
from oauth import google_bp
|
||||||
from commands import create_admin
|
from commands import create_admin
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-for-testing')
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///helpdesk.db'
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///helpdesk.db'
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
app.secret_key = 'your_secret_key' # Replace with a secure key
|
|
||||||
|
|
||||||
# Initialize the database with the Flask app
|
# Initialize the database with the Flask app
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
# Initialize Flask-Login
|
# Initialize Flask-Login
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager(app)
|
||||||
login_manager.init_app(app)
|
|
||||||
login_manager.login_view = 'auth.login'
|
login_manager.login_view = 'auth.login'
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(id):
|
def load_user(user_id):
|
||||||
return User.query.get(int(id))
|
return User.query.get(int(user_id))
|
||||||
|
|
||||||
# Register Blueprints
|
# Register Blueprints
|
||||||
app.register_blueprint(user_bp)
|
app.register_blueprint(user_bp)
|
||||||
app.register_blueprint(admin_bp, url_prefix='/admin')
|
app.register_blueprint(admin_bp, url_prefix='/admin')
|
||||||
app.register_blueprint(auth_bp, url_prefix='/auth')
|
app.register_blueprint(auth_bp, url_prefix='/auth')
|
||||||
|
app.register_blueprint(google_bp, url_prefix='/login')
|
||||||
|
|
||||||
# Create database tables if they don't exist
|
# Create database tables if they don't exist
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
@ -36,5 +42,13 @@ with app.app_context():
|
|||||||
# Add this after creating the app
|
# Add this after creating the app
|
||||||
app.cli.add_command(create_admin)
|
app.cli.add_command(create_admin)
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def page_not_found(e):
|
||||||
|
return render_template('error.html'), 404
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_server_error(e):
|
||||||
|
return render_template('error.html'), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
|
|||||||
6
auth.py
6
auth.py
@ -5,6 +5,7 @@ from models import db, User, SiteConfig
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||||||
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
|
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
|
||||||
|
from oauth import google_bp
|
||||||
|
|
||||||
auth_bp = Blueprint('auth', __name__)
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
@ -45,7 +46,10 @@ def login():
|
|||||||
return redirect(next_page or url_for('user.home'))
|
return redirect(next_page or url_for('user.home'))
|
||||||
flash('Invalid username or password', 'danger')
|
flash('Invalid username or password', 'danger')
|
||||||
|
|
||||||
return render_template('auth/login.html', form=form)
|
# Check if Google OAuth is configured
|
||||||
|
google_configured = google_bp.client_id is not None
|
||||||
|
|
||||||
|
return render_template('auth/login.html', form=form, google_configured=google_configured)
|
||||||
|
|
||||||
@auth_bp.route('/logout')
|
@auth_bp.route('/logout')
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
42
migrations/add_oauth_support.py
Normal file
42
migrations/add_oauth_support.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from app import app, db
|
||||||
|
from models import User, OAuth
|
||||||
|
|
||||||
|
def upgrade_database():
|
||||||
|
"""Add OAuth support to the database"""
|
||||||
|
with app.app_context():
|
||||||
|
# Check if columns already exist
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
# Check if is_oauth_user column exists in User table
|
||||||
|
result = conn.execute(db.text("PRAGMA table_info(user)"))
|
||||||
|
columns = result.fetchall()
|
||||||
|
column_names = [col[1] for col in columns]
|
||||||
|
|
||||||
|
if 'is_oauth_user' not in column_names:
|
||||||
|
conn.execute(db.text("ALTER TABLE user ADD COLUMN is_oauth_user BOOLEAN DEFAULT 0"))
|
||||||
|
print("Added is_oauth_user column to User table")
|
||||||
|
|
||||||
|
# Check if OAuth table exists
|
||||||
|
result = conn.execute(db.text("SELECT name FROM sqlite_master WHERE type='table' AND name='oauth'"))
|
||||||
|
table_exists = result.fetchone() is not None
|
||||||
|
|
||||||
|
if not table_exists:
|
||||||
|
# Create the OAuth table
|
||||||
|
conn.execute(db.text("""
|
||||||
|
CREATE TABLE oauth (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
provider VARCHAR(50) NOT NULL,
|
||||||
|
provider_user_id VARCHAR(256) NOT NULL,
|
||||||
|
token TEXT NOT NULL,
|
||||||
|
user_id INTEGER,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE,
|
||||||
|
UNIQUE (provider, provider_user_id)
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
print("Created OAuth table")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print("Database migration for OAuth support completed successfully")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
upgrade_database()
|
||||||
12
models.py
12
models.py
@ -1,22 +1,30 @@
|
|||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from flask_dance.consumer.storage.sqla import OAuthConsumerMixin
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
class User(db.Model, UserMixin):
|
class User(db.Model, UserMixin):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(64), unique=True, nullable=False)
|
username = db.Column(db.String(64), index=True, unique=True)
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
email = db.Column(db.String(120), index=True, unique=True)
|
||||||
password_hash = db.Column(db.String(128))
|
password_hash = db.Column(db.String(128))
|
||||||
is_admin = db.Column(db.Boolean, default=False)
|
is_admin = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
|
# New field to track if user was created via OAuth
|
||||||
|
is_oauth_user = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
self.password_hash = generate_password_hash(password)
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
class OAuth(OAuthConsumerMixin, db.Model):
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey(User.id))
|
||||||
|
user = db.relationship(User)
|
||||||
|
|
||||||
class NodeImage(db.Model):
|
class NodeImage(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
node_id = db.Column(db.Integer, db.ForeignKey('node.id'), nullable=False)
|
node_id = db.Column(db.Integer, db.ForeignKey('node.id'), nullable=False)
|
||||||
|
|||||||
90
oauth.py
Normal file
90
oauth.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import os
|
||||||
|
from flask import flash, redirect, url_for
|
||||||
|
from flask_dance.consumer import oauth_authorized
|
||||||
|
from flask_dance.consumer.storage.sqla import SQLAlchemyStorage
|
||||||
|
from flask_dance.contrib.google import make_google_blueprint
|
||||||
|
from flask_login import current_user, login_user
|
||||||
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
from models import db, User, OAuth
|
||||||
|
|
||||||
|
# Get Google OAuth credentials from environment variables
|
||||||
|
GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID")
|
||||||
|
GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET")
|
||||||
|
|
||||||
|
# Create the Google OAuth blueprint
|
||||||
|
google_bp = make_google_blueprint(
|
||||||
|
client_id=GOOGLE_CLIENT_ID,
|
||||||
|
client_secret=GOOGLE_CLIENT_SECRET,
|
||||||
|
scope=["profile", "email"],
|
||||||
|
storage=SQLAlchemyStorage(OAuth, db.session, user=current_user)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register a callback for when the OAuth login is successful
|
||||||
|
@oauth_authorized.connect_via(google_bp)
|
||||||
|
def google_logged_in(blueprint, token):
|
||||||
|
if not token:
|
||||||
|
flash("Failed to log in with Google.", "danger")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get the user's Google account info
|
||||||
|
resp = blueprint.session.get("/oauth2/v1/userinfo")
|
||||||
|
if not resp.ok:
|
||||||
|
flash("Failed to fetch user info from Google.", "danger")
|
||||||
|
return False
|
||||||
|
|
||||||
|
google_info = resp.json()
|
||||||
|
google_user_id = str(google_info["id"])
|
||||||
|
|
||||||
|
# Find this OAuth token in the database, or create it
|
||||||
|
query = OAuth.query.filter_by(
|
||||||
|
provider=blueprint.name,
|
||||||
|
provider_user_id=google_user_id
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
oauth = query.one()
|
||||||
|
except NoResultFound:
|
||||||
|
oauth = OAuth(
|
||||||
|
provider=blueprint.name,
|
||||||
|
provider_user_id=google_user_id,
|
||||||
|
token=token
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now, check to see if we already have a user with this email
|
||||||
|
if oauth.user:
|
||||||
|
# If this OAuth token already has an associated user, log them in
|
||||||
|
login_user(oauth.user)
|
||||||
|
flash("Successfully signed in with Google.", "success")
|
||||||
|
else:
|
||||||
|
# Check if we have a user with this email already
|
||||||
|
user = User.query.filter_by(email=google_info["email"]).first()
|
||||||
|
if user:
|
||||||
|
# If we do, link the OAuth token to the existing user
|
||||||
|
oauth.user = user
|
||||||
|
db.session.add(oauth)
|
||||||
|
db.session.commit()
|
||||||
|
login_user(user)
|
||||||
|
flash("Successfully signed in with Google.", "success")
|
||||||
|
else:
|
||||||
|
# If we don't, create a new user
|
||||||
|
username = google_info["email"].split("@")[0]
|
||||||
|
# Make sure username is unique
|
||||||
|
base_username = username
|
||||||
|
count = 1
|
||||||
|
while User.query.filter_by(username=username).first():
|
||||||
|
username = f"{base_username}{count}"
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
user = User(
|
||||||
|
username=username,
|
||||||
|
email=google_info["email"],
|
||||||
|
is_oauth_user=True
|
||||||
|
)
|
||||||
|
# No password is set for OAuth users
|
||||||
|
oauth.user = user
|
||||||
|
db.session.add_all([user, oauth])
|
||||||
|
db.session.commit()
|
||||||
|
login_user(user)
|
||||||
|
flash("Successfully signed in with Google.", "success")
|
||||||
|
|
||||||
|
# Prevent Flask-Dance from creating another token
|
||||||
|
return False
|
||||||
@ -5,6 +5,7 @@ Flask-Login==0.6.2
|
|||||||
Flask-WTF==1.2.1
|
Flask-WTF==1.2.1
|
||||||
Flask-Migrate==4.0.5
|
Flask-Migrate==4.0.5
|
||||||
Werkzeug==2.3.7
|
Werkzeug==2.3.7
|
||||||
|
Flask-Dance[sqla]==6.2.0
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
SQLAlchemy==2.0.23
|
SQLAlchemy==2.0.23
|
||||||
@ -16,6 +17,7 @@ email-validator==2.1.0
|
|||||||
# Security
|
# Security
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
bcrypt==4.0.1
|
bcrypt==4.0.1
|
||||||
|
blinker==1.6.2
|
||||||
|
|
||||||
# Image handling
|
# Image handling
|
||||||
Pillow==10.1.0
|
Pillow==10.1.0
|
||||||
@ -39,4 +41,8 @@ click==8.1.7
|
|||||||
|
|
||||||
# If you plan to use a different database than SQLite
|
# If you plan to use a different database than SQLite
|
||||||
# psycopg2-binary==2.9.9 # For PostgreSQL
|
# psycopg2-binary==2.9.9 # For PostgreSQL
|
||||||
# mysqlclient==2.2.0 # For MySQL
|
# mysqlclient==2.2.0 # For MySQL
|
||||||
|
|
||||||
|
# OAuth and SSO
|
||||||
|
Flask-Dance[sqla]==6.2.0
|
||||||
|
blinker==1.6.2
|
||||||
@ -33,6 +33,21 @@
|
|||||||
{{ form.submit(class="btn btn-primary") }}
|
{{ form.submit(class="btn btn-primary") }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% if google_configured %}
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<hr class="flex-grow-1">
|
||||||
|
<span class="mx-2 text-muted">OR</span>
|
||||||
|
<hr class="flex-grow-1">
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="{{ url_for('google.login') }}" class="btn btn-outline-danger">
|
||||||
|
<i class="fab fa-google me-2"></i> Sign in with Google
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
<p>New User? <a href="{{ url_for('auth.register') }}">Register here</a></p>
|
<p>New User? <a href="{{ url_for('auth.register') }}">Register here</a></p>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user