Spaces:
Sleeping
Sleeping
dhruv575 commited on
Commit ·
5854e13
1
Parent(s): 64725d7
Setup
Browse files- .gitignore +46 -0
- Dockerfile +34 -0
- README.md +87 -0
- app.py +88 -0
- controllers/__init__.py +1 -0
- controllers/department_controller.py +321 -0
- db.py +64 -0
- docker-compose.yml +25 -0
- models/__init__.py +7 -0
- models/department.py +127 -0
- models/incident.py +168 -0
- models/log.py +150 -0
- models/user.py +137 -0
- models/workflow.py +139 -0
- requirements.txt +16 -0
- routes/__init__.py +1 -0
- routes/department_routes.py +28 -0
- setup_env.py +41 -0
- test_department.py +47 -0
- utils/__init__.py +1 -0
- utils/auth.py +82 -0
.gitignore
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment variables
|
| 2 |
+
.env
|
| 3 |
+
|
| 4 |
+
# Python
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.py[cod]
|
| 7 |
+
*$py.class
|
| 8 |
+
*.so
|
| 9 |
+
.Python
|
| 10 |
+
env/
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
*.egg-info/
|
| 23 |
+
.installed.cfg
|
| 24 |
+
*.egg
|
| 25 |
+
|
| 26 |
+
# Virtual Environment
|
| 27 |
+
venv/
|
| 28 |
+
ENV/
|
| 29 |
+
|
| 30 |
+
# IDE specific files
|
| 31 |
+
.idea/
|
| 32 |
+
.vscode/
|
| 33 |
+
*.swp
|
| 34 |
+
*.swo
|
| 35 |
+
|
| 36 |
+
# Logs
|
| 37 |
+
logs/
|
| 38 |
+
*.log
|
| 39 |
+
|
| 40 |
+
# OS specific
|
| 41 |
+
.DS_Store
|
| 42 |
+
Thumbs.db
|
| 43 |
+
|
| 44 |
+
# Uploaded files and temporary files
|
| 45 |
+
uploads/
|
| 46 |
+
tmp/
|
Dockerfile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies including Tesseract
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
tesseract-ocr \
|
| 8 |
+
libmagic1 \
|
| 9 |
+
&& apt-get clean \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Copy requirements file and install Python dependencies
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
# Copy application code
|
| 17 |
+
COPY . .
|
| 18 |
+
|
| 19 |
+
# Create a .env file if not mounted externally (using env vars from compose)
|
| 20 |
+
RUN if [ ! -f .env ]; then \
|
| 21 |
+
echo "Creating default .env file" \
|
| 22 |
+
&& touch .env; \
|
| 23 |
+
fi
|
| 24 |
+
|
| 25 |
+
# Set environment variables
|
| 26 |
+
ENV FLASK_APP=app.py
|
| 27 |
+
ENV FLASK_ENV=production
|
| 28 |
+
ENV PYTHONUNBUFFERED=1
|
| 29 |
+
|
| 30 |
+
# Expose port for Flask
|
| 31 |
+
EXPOSE 5000
|
| 32 |
+
|
| 33 |
+
# Run the application
|
| 34 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
|
README.md
CHANGED
|
@@ -8,3 +8,90 @@ pinned: false
|
|
| 8 |
---
|
| 9 |
|
| 10 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 11 |
+
|
| 12 |
+
# Enflow Backend API
|
| 13 |
+
|
| 14 |
+
Enflow (Enforcement Workflow) Backend API that allows law enforcement agencies to build, maintain, and use workflows that automate tasks based on officers' daily logs.
|
| 15 |
+
|
| 16 |
+
## Setup Instructions
|
| 17 |
+
|
| 18 |
+
### Prerequisites
|
| 19 |
+
|
| 20 |
+
- Python 3.10 or newer
|
| 21 |
+
- Docker and Docker Compose (optional, for containerized deployment)
|
| 22 |
+
- MongoDB (we're using MongoDB Atlas in the current setup)
|
| 23 |
+
|
| 24 |
+
### Environment Setup
|
| 25 |
+
|
| 26 |
+
1. Clone the repository
|
| 27 |
+
2. Create a `.env` file in the backend directory with the following variables:
|
| 28 |
+
```
|
| 29 |
+
MONGO_URI=your_mongodb_connection_string
|
| 30 |
+
JWT_SECRET=your_jwt_secret
|
| 31 |
+
CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name
|
| 32 |
+
CLOUDINARY_API_KEY=your_cloudinary_api_key
|
| 33 |
+
CLOUDINARY_API_SECRET=your_cloudinary_api_secret
|
| 34 |
+
OPENAI_API_KEY=your_openai_api_key
|
| 35 |
+
FLASK_ENV=development
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
### Local Development
|
| 39 |
+
|
| 40 |
+
1. Create and activate a virtual environment:
|
| 41 |
+
```
|
| 42 |
+
python -m venv venv
|
| 43 |
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
2. Install dependencies:
|
| 47 |
+
```
|
| 48 |
+
pip install -r requirements.txt
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
3. Run the application:
|
| 52 |
+
```
|
| 53 |
+
python app.py
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
4. The API will be available at http://localhost:5000
|
| 57 |
+
|
| 58 |
+
### Docker Deployment
|
| 59 |
+
|
| 60 |
+
1. Build and start the containers:
|
| 61 |
+
```
|
| 62 |
+
docker-compose up -d
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
2. The API will be available at http://localhost:5000
|
| 66 |
+
|
| 67 |
+
## API Documentation
|
| 68 |
+
|
| 69 |
+
### Department Endpoints
|
| 70 |
+
|
| 71 |
+
- `POST /api/departments` - Create a new department with its first admin user
|
| 72 |
+
- `GET /api/departments` - Get all departments (requires authentication)
|
| 73 |
+
- `GET /api/departments/{department_id}` - Get department by ID (requires authentication)
|
| 74 |
+
- `PUT /api/departments/{department_id}` - Update department details (requires admin)
|
| 75 |
+
- `DELETE /api/departments/{department_id}` - Delete a department (requires admin)
|
| 76 |
+
- `GET /api/departments/{department_id}/members` - Get all members of a department (requires authentication)
|
| 77 |
+
- `POST /api/departments/{department_id}/members` - Add a member to a department (requires admin)
|
| 78 |
+
- `POST /api/departments/{department_id}/members/csv` - Add multiple members via CSV upload (requires admin)
|
| 79 |
+
- `DELETE /api/departments/{department_id}/members/{user_id}` - Remove a member from a department (requires admin)
|
| 80 |
+
- `PUT /api/departments/{department_id}/members/{user_id}/permissions` - Update a member's permissions (requires admin)
|
| 81 |
+
|
| 82 |
+
### Testing
|
| 83 |
+
|
| 84 |
+
To test the department creation functionality, run:
|
| 85 |
+
|
| 86 |
+
```
|
| 87 |
+
python test_department.py
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
## Project Structure
|
| 91 |
+
|
| 92 |
+
- `app.py` - Main Flask application
|
| 93 |
+
- `db.py` - Database connection and utilities
|
| 94 |
+
- `models/` - Data models
|
| 95 |
+
- `controllers/` - Controller functions
|
| 96 |
+
- `routes/` - API route definitions
|
| 97 |
+
- `utils/` - Utility functions and middleware
|
app.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from flask import Flask, jsonify, request
|
| 3 |
+
from flask_cors import CORS
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
import logging
|
| 6 |
+
from logging.handlers import RotatingFileHandler
|
| 7 |
+
import pymongo
|
| 8 |
+
import cloudinary
|
| 9 |
+
import cloudinary.uploader
|
| 10 |
+
|
| 11 |
+
# Import routes
|
| 12 |
+
from routes.department_routes import department_bp
|
| 13 |
+
|
| 14 |
+
# Load environment variables
|
| 15 |
+
load_dotenv()
|
| 16 |
+
|
| 17 |
+
# Create Flask app
|
| 18 |
+
app = Flask(__name__)
|
| 19 |
+
app.config['SECRET_KEY'] = os.environ.get('JWT_SECRET')
|
| 20 |
+
app.config['MONGO_URI'] = os.environ.get('MONGO_URI')
|
| 21 |
+
|
| 22 |
+
# Configure Cloudinary
|
| 23 |
+
cloudinary.config(
|
| 24 |
+
cloud_name=os.environ.get('CLOUDINARY_CLOUD_NAME'),
|
| 25 |
+
api_key=os.environ.get('CLOUDINARY_API_KEY'),
|
| 26 |
+
api_secret=os.environ.get('CLOUDINARY_API_SECRET')
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Enable CORS
|
| 30 |
+
CORS(app)
|
| 31 |
+
|
| 32 |
+
# Register blueprints
|
| 33 |
+
app.register_blueprint(department_bp, url_prefix='/api/departments')
|
| 34 |
+
# Other blueprints will be registered later
|
| 35 |
+
|
| 36 |
+
# Create uploads directory if it doesn't exist
|
| 37 |
+
if not os.path.exists('uploads'):
|
| 38 |
+
os.makedirs('uploads')
|
| 39 |
+
app.config['UPLOAD_FOLDER'] = 'uploads'
|
| 40 |
+
|
| 41 |
+
# Error handler
|
| 42 |
+
@app.errorhandler(Exception)
|
| 43 |
+
def handle_exception(e):
|
| 44 |
+
app.logger.error(f"Unhandled exception: {str(e)}")
|
| 45 |
+
return jsonify({"error": "An unexpected error occurred"}), 500
|
| 46 |
+
|
| 47 |
+
# Root route
|
| 48 |
+
@app.route('/')
|
| 49 |
+
def index():
|
| 50 |
+
return jsonify({
|
| 51 |
+
"message": "Enflow API is running",
|
| 52 |
+
"version": "1.0.0"
|
| 53 |
+
})
|
| 54 |
+
|
| 55 |
+
# Health check route
|
| 56 |
+
@app.route('/health')
|
| 57 |
+
def health_check():
|
| 58 |
+
try:
|
| 59 |
+
# Check MongoDB connection
|
| 60 |
+
from db import Database
|
| 61 |
+
db = Database.get_instance().get_db()
|
| 62 |
+
db.command('ping')
|
| 63 |
+
|
| 64 |
+
# All checks passed
|
| 65 |
+
return jsonify({
|
| 66 |
+
"status": "healthy",
|
| 67 |
+
"mongo": "connected"
|
| 68 |
+
}), 200
|
| 69 |
+
except Exception as e:
|
| 70 |
+
app.logger.error(f"Health check failed: {str(e)}")
|
| 71 |
+
return jsonify({
|
| 72 |
+
"status": "unhealthy",
|
| 73 |
+
"error": str(e)
|
| 74 |
+
}), 500
|
| 75 |
+
|
| 76 |
+
# Setup logging
|
| 77 |
+
if not os.path.exists('logs'):
|
| 78 |
+
os.makedirs('logs')
|
| 79 |
+
|
| 80 |
+
handler = RotatingFileHandler('logs/app.log', maxBytes=10000, backupCount=3)
|
| 81 |
+
handler.setLevel(logging.INFO)
|
| 82 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 83 |
+
handler.setFormatter(formatter)
|
| 84 |
+
app.logger.addHandler(handler)
|
| 85 |
+
app.logger.setLevel(logging.INFO)
|
| 86 |
+
|
| 87 |
+
if __name__ == '__main__':
|
| 88 |
+
app.run(host='0.0.0.0', port=5000, debug=os.environ.get('FLASK_ENV') == 'development')
|
controllers/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Controllers package initialization
|
controllers/department_controller.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import jsonify, request
|
| 2 |
+
from models.department import Department
|
| 3 |
+
from models.user import User
|
| 4 |
+
import csv
|
| 5 |
+
import io
|
| 6 |
+
import bcrypt
|
| 7 |
+
import random
|
| 8 |
+
import string
|
| 9 |
+
import logging
|
| 10 |
+
|
| 11 |
+
# Configure logging
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
def generate_random_password(length=12):
|
| 15 |
+
"""Generate a random password"""
|
| 16 |
+
characters = string.ascii_letters + string.digits
|
| 17 |
+
return ''.join(random.choice(characters) for _ in range(length))
|
| 18 |
+
|
| 19 |
+
def create_department():
|
| 20 |
+
"""Create a new department with its first admin user"""
|
| 21 |
+
data = request.get_json()
|
| 22 |
+
|
| 23 |
+
# Check if required fields are present
|
| 24 |
+
required_fields = ['name', 'address', 'website', 'admin_email', 'admin_name', 'admin_password']
|
| 25 |
+
for field in required_fields:
|
| 26 |
+
if field not in data:
|
| 27 |
+
return jsonify({'message': f'Missing required field: {field}'}), 400
|
| 28 |
+
|
| 29 |
+
# Check if department with this name already exists
|
| 30 |
+
existing_department = Department.find_by_name(data['name'])
|
| 31 |
+
if existing_department:
|
| 32 |
+
return jsonify({'message': 'Department with this name already exists'}), 400
|
| 33 |
+
|
| 34 |
+
# Check if user with this email already exists
|
| 35 |
+
existing_user = User.find_by_email(data['admin_email'])
|
| 36 |
+
if existing_user:
|
| 37 |
+
return jsonify({'message': 'User with this email already exists'}), 400
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
# Create department
|
| 41 |
+
department = Department(
|
| 42 |
+
name=data['name'],
|
| 43 |
+
address=data['address'],
|
| 44 |
+
website=data['website']
|
| 45 |
+
)
|
| 46 |
+
department.save()
|
| 47 |
+
|
| 48 |
+
# Create admin user
|
| 49 |
+
hashed_password = User.hash_password(data['admin_password'])
|
| 50 |
+
admin_user = User(
|
| 51 |
+
email=data['admin_email'],
|
| 52 |
+
name=data['admin_name'],
|
| 53 |
+
password=hashed_password,
|
| 54 |
+
permissions='Admin',
|
| 55 |
+
position=data.get('admin_position', 'Administrator'),
|
| 56 |
+
department_id=department._id
|
| 57 |
+
)
|
| 58 |
+
admin_user.save()
|
| 59 |
+
|
| 60 |
+
# Add admin to department members
|
| 61 |
+
department.add_member(admin_user._id)
|
| 62 |
+
|
| 63 |
+
# Return success response
|
| 64 |
+
return jsonify({
|
| 65 |
+
'message': 'Department and admin user created successfully',
|
| 66 |
+
'department': department.to_dict(),
|
| 67 |
+
'admin_user': admin_user.to_dict()
|
| 68 |
+
}), 201
|
| 69 |
+
|
| 70 |
+
except Exception as e:
|
| 71 |
+
logger.error(f"Error creating department: {str(e)}")
|
| 72 |
+
return jsonify({'message': f'Error creating department: {str(e)}'}), 500
|
| 73 |
+
|
| 74 |
+
def get_department(department_id):
|
| 75 |
+
"""Get department by ID"""
|
| 76 |
+
department = Department.find_by_id(department_id)
|
| 77 |
+
if not department:
|
| 78 |
+
return jsonify({'message': 'Department not found'}), 404
|
| 79 |
+
|
| 80 |
+
return jsonify({'department': department.to_dict()}), 200
|
| 81 |
+
|
| 82 |
+
def update_department(department_id):
|
| 83 |
+
"""Update department details"""
|
| 84 |
+
department = Department.find_by_id(department_id)
|
| 85 |
+
if not department:
|
| 86 |
+
return jsonify({'message': 'Department not found'}), 404
|
| 87 |
+
|
| 88 |
+
data = request.get_json()
|
| 89 |
+
|
| 90 |
+
# Update fields if provided
|
| 91 |
+
if 'name' in data:
|
| 92 |
+
department.name = data['name']
|
| 93 |
+
if 'address' in data:
|
| 94 |
+
department.address = data['address']
|
| 95 |
+
if 'website' in data:
|
| 96 |
+
department.website = data['website']
|
| 97 |
+
|
| 98 |
+
# Save changes
|
| 99 |
+
if department.save():
|
| 100 |
+
return jsonify({'message': 'Department updated successfully', 'department': department.to_dict()}), 200
|
| 101 |
+
else:
|
| 102 |
+
return jsonify({'message': 'Failed to update department'}), 500
|
| 103 |
+
|
| 104 |
+
def delete_department(department_id):
|
| 105 |
+
"""Delete a department"""
|
| 106 |
+
department = Department.find_by_id(department_id)
|
| 107 |
+
if not department:
|
| 108 |
+
return jsonify({'message': 'Department not found'}), 404
|
| 109 |
+
|
| 110 |
+
# Delete all users in the department
|
| 111 |
+
users = User.find_by_department(department_id)
|
| 112 |
+
for user in users:
|
| 113 |
+
user.delete()
|
| 114 |
+
|
| 115 |
+
# Delete the department
|
| 116 |
+
if department.delete():
|
| 117 |
+
return jsonify({'message': 'Department and all its users deleted successfully'}), 200
|
| 118 |
+
else:
|
| 119 |
+
return jsonify({'message': 'Failed to delete department'}), 500
|
| 120 |
+
|
| 121 |
+
def get_all_departments():
|
| 122 |
+
"""Get all departments"""
|
| 123 |
+
departments = Department.get_all()
|
| 124 |
+
return jsonify({'departments': [department.to_dict() for department in departments]}), 200
|
| 125 |
+
|
| 126 |
+
def add_members_csv(department_id):
|
| 127 |
+
"""Add multiple members to a department using CSV upload"""
|
| 128 |
+
department = Department.find_by_id(department_id)
|
| 129 |
+
if not department:
|
| 130 |
+
return jsonify({'message': 'Department not found'}), 404
|
| 131 |
+
|
| 132 |
+
if 'file' not in request.files:
|
| 133 |
+
return jsonify({'message': 'No file part'}), 400
|
| 134 |
+
|
| 135 |
+
file = request.files['file']
|
| 136 |
+
if file.filename == '':
|
| 137 |
+
return jsonify({'message': 'No selected file'}), 400
|
| 138 |
+
|
| 139 |
+
if file and file.filename.endswith('.csv'):
|
| 140 |
+
try:
|
| 141 |
+
# Read CSV data
|
| 142 |
+
csv_data = file.read().decode('utf-8')
|
| 143 |
+
csv_reader = csv.DictReader(io.StringIO(csv_data))
|
| 144 |
+
|
| 145 |
+
# Process each row
|
| 146 |
+
new_users = []
|
| 147 |
+
errors = []
|
| 148 |
+
|
| 149 |
+
required_fields = ['email', 'name', 'position', 'permissions']
|
| 150 |
+
|
| 151 |
+
for row_index, row in enumerate(csv_reader, start=1):
|
| 152 |
+
# Check if all required fields are present
|
| 153 |
+
missing_fields = [field for field in required_fields if field not in row or not row[field]]
|
| 154 |
+
|
| 155 |
+
if missing_fields:
|
| 156 |
+
errors.append(f"Row {row_index}: Missing required fields: {', '.join(missing_fields)}")
|
| 157 |
+
continue
|
| 158 |
+
|
| 159 |
+
# Check if user already exists
|
| 160 |
+
if User.find_by_email(row['email']):
|
| 161 |
+
errors.append(f"Row {row_index}: User with email {row['email']} already exists")
|
| 162 |
+
continue
|
| 163 |
+
|
| 164 |
+
# Check if permissions are valid
|
| 165 |
+
if row['permissions'] not in ['Admin', 'User']:
|
| 166 |
+
row['permissions'] = 'User' # Default to User if invalid
|
| 167 |
+
|
| 168 |
+
# Generate random password
|
| 169 |
+
password = generate_random_password()
|
| 170 |
+
hashed_password = User.hash_password(password)
|
| 171 |
+
|
| 172 |
+
# Create new user
|
| 173 |
+
user = User(
|
| 174 |
+
email=row['email'],
|
| 175 |
+
name=row['name'],
|
| 176 |
+
password=hashed_password,
|
| 177 |
+
permissions=row['permissions'],
|
| 178 |
+
position=row['position'],
|
| 179 |
+
department_id=department._id
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
if user.save():
|
| 183 |
+
# Add user to department
|
| 184 |
+
department.add_member(user._id)
|
| 185 |
+
|
| 186 |
+
# Store for response (including the raw password)
|
| 187 |
+
user_dict = user.to_dict()
|
| 188 |
+
user_dict['raw_password'] = password
|
| 189 |
+
new_users.append(user_dict)
|
| 190 |
+
else:
|
| 191 |
+
errors.append(f"Row {row_index}: Failed to save user {row['email']}")
|
| 192 |
+
|
| 193 |
+
# Return results
|
| 194 |
+
return jsonify({
|
| 195 |
+
'message': f"Processed {len(new_users)} new users with {len(errors)} errors",
|
| 196 |
+
'new_users': new_users,
|
| 197 |
+
'errors': errors
|
| 198 |
+
}), 200
|
| 199 |
+
|
| 200 |
+
except Exception as e:
|
| 201 |
+
logger.error(f"Error processing CSV: {str(e)}")
|
| 202 |
+
return jsonify({'message': f'Error processing CSV: {str(e)}'}), 500
|
| 203 |
+
else:
|
| 204 |
+
return jsonify({'message': 'File must be a CSV'}), 400
|
| 205 |
+
|
| 206 |
+
def add_member(department_id):
|
| 207 |
+
"""Add a single member to a department"""
|
| 208 |
+
department = Department.find_by_id(department_id)
|
| 209 |
+
if not department:
|
| 210 |
+
return jsonify({'message': 'Department not found'}), 404
|
| 211 |
+
|
| 212 |
+
data = request.get_json()
|
| 213 |
+
|
| 214 |
+
# Check if required fields are present
|
| 215 |
+
required_fields = ['email', 'name', 'position', 'permissions']
|
| 216 |
+
for field in required_fields:
|
| 217 |
+
if field not in data:
|
| 218 |
+
return jsonify({'message': f'Missing required field: {field}'}), 400
|
| 219 |
+
|
| 220 |
+
# Check if user already exists
|
| 221 |
+
if User.find_by_email(data['email']):
|
| 222 |
+
return jsonify({'message': 'User with this email already exists'}), 400
|
| 223 |
+
|
| 224 |
+
# Check if permissions are valid
|
| 225 |
+
if data['permissions'] not in ['Admin', 'User']:
|
| 226 |
+
data['permissions'] = 'User' # Default to User if invalid
|
| 227 |
+
|
| 228 |
+
try:
|
| 229 |
+
# Generate random password if not provided
|
| 230 |
+
password = data.get('password', generate_random_password())
|
| 231 |
+
hashed_password = User.hash_password(password)
|
| 232 |
+
|
| 233 |
+
# Create new user
|
| 234 |
+
user = User(
|
| 235 |
+
email=data['email'],
|
| 236 |
+
name=data['name'],
|
| 237 |
+
password=hashed_password,
|
| 238 |
+
permissions=data['permissions'],
|
| 239 |
+
position=data['position'],
|
| 240 |
+
department_id=department._id
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
if user.save():
|
| 244 |
+
# Add user to department
|
| 245 |
+
department.add_member(user._id)
|
| 246 |
+
|
| 247 |
+
# Return the raw password in the response
|
| 248 |
+
user_dict = user.to_dict()
|
| 249 |
+
user_dict['raw_password'] = password
|
| 250 |
+
|
| 251 |
+
return jsonify({
|
| 252 |
+
'message': 'User added successfully',
|
| 253 |
+
'user': user_dict
|
| 254 |
+
}), 201
|
| 255 |
+
else:
|
| 256 |
+
return jsonify({'message': 'Failed to save user'}), 500
|
| 257 |
+
|
| 258 |
+
except Exception as e:
|
| 259 |
+
logger.error(f"Error adding member: {str(e)}")
|
| 260 |
+
return jsonify({'message': f'Error adding member: {str(e)}'}), 500
|
| 261 |
+
|
| 262 |
+
def get_department_members(department_id):
|
| 263 |
+
"""Get all members of a department"""
|
| 264 |
+
department = Department.find_by_id(department_id)
|
| 265 |
+
if not department:
|
| 266 |
+
return jsonify({'message': 'Department not found'}), 404
|
| 267 |
+
|
| 268 |
+
users = User.find_by_department(department_id)
|
| 269 |
+
return jsonify({'members': [user.to_dict() for user in users]}), 200
|
| 270 |
+
|
| 271 |
+
def remove_member(department_id, user_id):
|
| 272 |
+
"""Remove a member from a department"""
|
| 273 |
+
department = Department.find_by_id(department_id)
|
| 274 |
+
if not department:
|
| 275 |
+
return jsonify({'message': 'Department not found'}), 404
|
| 276 |
+
|
| 277 |
+
user = User.find_by_id(user_id)
|
| 278 |
+
if not user:
|
| 279 |
+
return jsonify({'message': 'User not found'}), 404
|
| 280 |
+
|
| 281 |
+
# Check if user belongs to the department
|
| 282 |
+
if str(user.department_id) != str(department._id):
|
| 283 |
+
return jsonify({'message': 'User does not belong to this department'}), 400
|
| 284 |
+
|
| 285 |
+
# Remove user from department
|
| 286 |
+
if department.remove_member(user_id) and user.delete():
|
| 287 |
+
return jsonify({'message': 'User removed successfully'}), 200
|
| 288 |
+
else:
|
| 289 |
+
return jsonify({'message': 'Failed to remove user'}), 500
|
| 290 |
+
|
| 291 |
+
def update_member_permissions(department_id, user_id):
|
| 292 |
+
"""Update a member's permissions"""
|
| 293 |
+
department = Department.find_by_id(department_id)
|
| 294 |
+
if not department:
|
| 295 |
+
return jsonify({'message': 'Department not found'}), 404
|
| 296 |
+
|
| 297 |
+
user = User.find_by_id(user_id)
|
| 298 |
+
if not user:
|
| 299 |
+
return jsonify({'message': 'User not found'}), 404
|
| 300 |
+
|
| 301 |
+
# Check if user belongs to the department
|
| 302 |
+
if str(user.department_id) != str(department._id):
|
| 303 |
+
return jsonify({'message': 'User does not belong to this department'}), 400
|
| 304 |
+
|
| 305 |
+
data = request.get_json()
|
| 306 |
+
|
| 307 |
+
# Check if permissions are provided
|
| 308 |
+
if 'permissions' not in data:
|
| 309 |
+
return jsonify({'message': 'Permissions not provided'}), 400
|
| 310 |
+
|
| 311 |
+
# Check if permissions are valid
|
| 312 |
+
if data['permissions'] not in ['Admin', 'User']:
|
| 313 |
+
return jsonify({'message': 'Invalid permissions. Must be "Admin" or "User"'}), 400
|
| 314 |
+
|
| 315 |
+
# Update permissions
|
| 316 |
+
user.permissions = data['permissions']
|
| 317 |
+
|
| 318 |
+
if user.save():
|
| 319 |
+
return jsonify({'message': 'User permissions updated successfully', 'user': user.to_dict()}), 200
|
| 320 |
+
else:
|
| 321 |
+
return jsonify({'message': 'Failed to update user permissions'}), 500
|
db.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import logging
|
| 3 |
+
from pymongo import MongoClient
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
# Load environment variables
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
# Set up logging
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
class Database:
|
| 13 |
+
_instance = None
|
| 14 |
+
|
| 15 |
+
@classmethod
|
| 16 |
+
def get_instance(cls):
|
| 17 |
+
if cls._instance is None:
|
| 18 |
+
cls._instance = cls()
|
| 19 |
+
return cls._instance
|
| 20 |
+
|
| 21 |
+
def __init__(self):
|
| 22 |
+
if Database._instance is not None:
|
| 23 |
+
raise Exception("This class is a singleton!")
|
| 24 |
+
|
| 25 |
+
self.mongo_uri = os.environ.get('MONGO_URI')
|
| 26 |
+
if not self.mongo_uri:
|
| 27 |
+
logger.error("MONGO_URI environment variable not set")
|
| 28 |
+
raise ValueError("MONGO_URI environment variable not set")
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
self.client = MongoClient(self.mongo_uri)
|
| 32 |
+
self.db = self.client['enflow']
|
| 33 |
+
logger.info("Connected to MongoDB successfully")
|
| 34 |
+
except Exception as e:
|
| 35 |
+
logger.error(f"Error connecting to MongoDB: {str(e)}")
|
| 36 |
+
raise
|
| 37 |
+
|
| 38 |
+
def get_db(self):
|
| 39 |
+
return self.db
|
| 40 |
+
|
| 41 |
+
def close(self):
|
| 42 |
+
if hasattr(self, 'client'):
|
| 43 |
+
self.client.close()
|
| 44 |
+
logger.info("MongoDB connection closed")
|
| 45 |
+
|
| 46 |
+
# Helper functions to get collections
|
| 47 |
+
def get_collection(collection_name):
|
| 48 |
+
db = Database.get_instance().get_db()
|
| 49 |
+
return db[collection_name]
|
| 50 |
+
|
| 51 |
+
def get_users_collection():
|
| 52 |
+
return get_collection('users')
|
| 53 |
+
|
| 54 |
+
def get_departments_collection():
|
| 55 |
+
return get_collection('departments')
|
| 56 |
+
|
| 57 |
+
def get_workflows_collection():
|
| 58 |
+
return get_collection('workflows')
|
| 59 |
+
|
| 60 |
+
def get_logs_collection():
|
| 61 |
+
return get_collection('logs')
|
| 62 |
+
|
| 63 |
+
def get_incidents_collection():
|
| 64 |
+
return get_collection('incidents')
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
app:
|
| 5 |
+
build: .
|
| 6 |
+
ports:
|
| 7 |
+
- "5000:5000"
|
| 8 |
+
volumes:
|
| 9 |
+
- .:/app
|
| 10 |
+
env_file:
|
| 11 |
+
- .env
|
| 12 |
+
depends_on:
|
| 13 |
+
- redis
|
| 14 |
+
restart: unless-stopped
|
| 15 |
+
|
| 16 |
+
redis:
|
| 17 |
+
image: redis:7-alpine
|
| 18 |
+
ports:
|
| 19 |
+
- "6379:6379"
|
| 20 |
+
volumes:
|
| 21 |
+
- redis_data:/data
|
| 22 |
+
restart: unless-stopped
|
| 23 |
+
|
| 24 |
+
volumes:
|
| 25 |
+
redis_data:
|
models/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from models.user import User
|
| 2 |
+
from models.department import Department
|
| 3 |
+
from models.workflow import Workflow
|
| 4 |
+
from models.log import Log
|
| 5 |
+
from models.incident import Incident
|
| 6 |
+
|
| 7 |
+
__all__ = ['User', 'Department', 'Workflow', 'Log', 'Incident']
|
models/department.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from bson import ObjectId
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from db import get_departments_collection
|
| 4 |
+
|
| 5 |
+
class Department:
|
| 6 |
+
def __init__(self, name, address, website, members=None, workflows=None, _id=None, created_at=None, updated_at=None):
|
| 7 |
+
self.name = name
|
| 8 |
+
self.address = address
|
| 9 |
+
self.website = website
|
| 10 |
+
self.members = members or []
|
| 11 |
+
self.workflows = workflows or []
|
| 12 |
+
self._id = _id
|
| 13 |
+
self.created_at = created_at or datetime.now()
|
| 14 |
+
self.updated_at = updated_at or datetime.now()
|
| 15 |
+
|
| 16 |
+
def to_dict(self):
|
| 17 |
+
"""Convert instance to dictionary"""
|
| 18 |
+
department_dict = {
|
| 19 |
+
"name": self.name,
|
| 20 |
+
"address": self.address,
|
| 21 |
+
"website": self.website,
|
| 22 |
+
"members": [str(member_id) for member_id in self.members],
|
| 23 |
+
"workflows": [str(workflow_id) for workflow_id in self.workflows],
|
| 24 |
+
"created_at": self.created_at,
|
| 25 |
+
"updated_at": self.updated_at
|
| 26 |
+
}
|
| 27 |
+
if self._id:
|
| 28 |
+
department_dict["_id"] = str(self._id)
|
| 29 |
+
return department_dict
|
| 30 |
+
|
| 31 |
+
@classmethod
|
| 32 |
+
def from_dict(cls, department_dict):
|
| 33 |
+
"""Create instance from dictionary"""
|
| 34 |
+
if "_id" in department_dict and department_dict["_id"]:
|
| 35 |
+
department_dict["_id"] = ObjectId(department_dict["_id"]) if isinstance(department_dict["_id"], str) else department_dict["_id"]
|
| 36 |
+
|
| 37 |
+
# Convert string IDs to ObjectIds for members and workflows
|
| 38 |
+
if "members" in department_dict and department_dict["members"]:
|
| 39 |
+
department_dict["members"] = [ObjectId(member_id) if isinstance(member_id, str) else member_id for member_id in department_dict["members"]]
|
| 40 |
+
|
| 41 |
+
if "workflows" in department_dict and department_dict["workflows"]:
|
| 42 |
+
department_dict["workflows"] = [ObjectId(workflow_id) if isinstance(workflow_id, str) else workflow_id for workflow_id in department_dict["workflows"]]
|
| 43 |
+
|
| 44 |
+
return cls(**department_dict)
|
| 45 |
+
|
| 46 |
+
def save(self):
|
| 47 |
+
"""Save department to database"""
|
| 48 |
+
departments_collection = get_departments_collection()
|
| 49 |
+
department_dict = self.to_dict()
|
| 50 |
+
|
| 51 |
+
if self._id:
|
| 52 |
+
# Update existing department
|
| 53 |
+
department_dict["updated_at"] = datetime.now()
|
| 54 |
+
result = departments_collection.update_one(
|
| 55 |
+
{"_id": ObjectId(self._id)},
|
| 56 |
+
{"$set": department_dict}
|
| 57 |
+
)
|
| 58 |
+
return result.modified_count > 0
|
| 59 |
+
else:
|
| 60 |
+
# Insert new department
|
| 61 |
+
department_dict["created_at"] = datetime.now()
|
| 62 |
+
department_dict["updated_at"] = datetime.now()
|
| 63 |
+
result = departments_collection.insert_one(department_dict)
|
| 64 |
+
self._id = result.inserted_id
|
| 65 |
+
return result.acknowledged
|
| 66 |
+
|
| 67 |
+
@classmethod
|
| 68 |
+
def find_by_id(cls, department_id):
|
| 69 |
+
"""Find department by ID"""
|
| 70 |
+
departments_collection = get_departments_collection()
|
| 71 |
+
department_data = departments_collection.find_one({"_id": ObjectId(department_id)})
|
| 72 |
+
if department_data:
|
| 73 |
+
return cls.from_dict(department_data)
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
@classmethod
|
| 77 |
+
def find_by_name(cls, name):
|
| 78 |
+
"""Find department by name"""
|
| 79 |
+
departments_collection = get_departments_collection()
|
| 80 |
+
department_data = departments_collection.find_one({"name": name})
|
| 81 |
+
if department_data:
|
| 82 |
+
return cls.from_dict(department_data)
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
@classmethod
|
| 86 |
+
def get_all(cls):
|
| 87 |
+
"""Get all departments"""
|
| 88 |
+
departments_collection = get_departments_collection()
|
| 89 |
+
departments_data = departments_collection.find()
|
| 90 |
+
return [cls.from_dict(department_data) for department_data in departments_data]
|
| 91 |
+
|
| 92 |
+
def delete(self):
|
| 93 |
+
"""Delete department from database"""
|
| 94 |
+
if not self._id:
|
| 95 |
+
return False
|
| 96 |
+
|
| 97 |
+
departments_collection = get_departments_collection()
|
| 98 |
+
result = departments_collection.delete_one({"_id": ObjectId(self._id)})
|
| 99 |
+
return result.deleted_count > 0
|
| 100 |
+
|
| 101 |
+
def add_member(self, user_id):
|
| 102 |
+
"""Add a member to department"""
|
| 103 |
+
if user_id not in self.members:
|
| 104 |
+
self.members.append(ObjectId(user_id))
|
| 105 |
+
return self.save()
|
| 106 |
+
return True
|
| 107 |
+
|
| 108 |
+
def remove_member(self, user_id):
|
| 109 |
+
"""Remove a member from department"""
|
| 110 |
+
if ObjectId(user_id) in self.members:
|
| 111 |
+
self.members.remove(ObjectId(user_id))
|
| 112 |
+
return self.save()
|
| 113 |
+
return True
|
| 114 |
+
|
| 115 |
+
def add_workflow(self, workflow_id):
|
| 116 |
+
"""Add a workflow to department"""
|
| 117 |
+
if workflow_id not in self.workflows:
|
| 118 |
+
self.workflows.append(ObjectId(workflow_id))
|
| 119 |
+
return self.save()
|
| 120 |
+
return True
|
| 121 |
+
|
| 122 |
+
def remove_workflow(self, workflow_id):
|
| 123 |
+
"""Remove a workflow from department"""
|
| 124 |
+
if ObjectId(workflow_id) in self.workflows:
|
| 125 |
+
self.workflows.remove(ObjectId(workflow_id))
|
| 126 |
+
return self.save()
|
| 127 |
+
return True
|
models/incident.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from bson import ObjectId
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from db import get_incidents_collection
|
| 4 |
+
|
| 5 |
+
class Incident:
|
| 6 |
+
def __init__(self, department_id, user_id, workflow_id, description, incident_date,
|
| 7 |
+
filled_forms=None, log_id=None, activity_text=None, form_data=None, _id=None,
|
| 8 |
+
created_at=None, updated_at=None, processing_status="pending"):
|
| 9 |
+
self.department_id = department_id
|
| 10 |
+
self.user_id = user_id
|
| 11 |
+
self.workflow_id = workflow_id
|
| 12 |
+
self.description = description
|
| 13 |
+
self.incident_date = incident_date
|
| 14 |
+
self.filled_forms = filled_forms or [] # List of file paths to filled forms
|
| 15 |
+
self.log_id = log_id # Source log
|
| 16 |
+
self.activity_text = activity_text # Original text from the log that triggered this incident
|
| 17 |
+
self.form_data = form_data or {} # Extracted data to fill forms
|
| 18 |
+
self._id = _id
|
| 19 |
+
self.created_at = created_at or datetime.now()
|
| 20 |
+
self.updated_at = updated_at or datetime.now()
|
| 21 |
+
self.processing_status = processing_status # "pending", "processing", "completed", "failed"
|
| 22 |
+
|
| 23 |
+
def to_dict(self):
|
| 24 |
+
"""Convert instance to dictionary"""
|
| 25 |
+
incident_dict = {
|
| 26 |
+
"department_id": str(self.department_id) if self.department_id else None,
|
| 27 |
+
"user_id": str(self.user_id) if self.user_id else None,
|
| 28 |
+
"workflow_id": str(self.workflow_id) if self.workflow_id else None,
|
| 29 |
+
"description": self.description,
|
| 30 |
+
"incident_date": self.incident_date,
|
| 31 |
+
"filled_forms": self.filled_forms,
|
| 32 |
+
"log_id": str(self.log_id) if self.log_id else None,
|
| 33 |
+
"activity_text": self.activity_text,
|
| 34 |
+
"form_data": self.form_data,
|
| 35 |
+
"created_at": self.created_at,
|
| 36 |
+
"updated_at": self.updated_at,
|
| 37 |
+
"processing_status": self.processing_status
|
| 38 |
+
}
|
| 39 |
+
if self._id:
|
| 40 |
+
incident_dict["_id"] = str(self._id)
|
| 41 |
+
return incident_dict
|
| 42 |
+
|
| 43 |
+
@classmethod
|
| 44 |
+
def from_dict(cls, incident_dict):
|
| 45 |
+
"""Create instance from dictionary"""
|
| 46 |
+
if "_id" in incident_dict and incident_dict["_id"]:
|
| 47 |
+
incident_dict["_id"] = ObjectId(incident_dict["_id"]) if isinstance(incident_dict["_id"], str) else incident_dict["_id"]
|
| 48 |
+
|
| 49 |
+
if "department_id" in incident_dict and incident_dict["department_id"]:
|
| 50 |
+
incident_dict["department_id"] = ObjectId(incident_dict["department_id"]) if isinstance(incident_dict["department_id"], str) else incident_dict["department_id"]
|
| 51 |
+
|
| 52 |
+
if "user_id" in incident_dict and incident_dict["user_id"]:
|
| 53 |
+
incident_dict["user_id"] = ObjectId(incident_dict["user_id"]) if isinstance(incident_dict["user_id"], str) else incident_dict["user_id"]
|
| 54 |
+
|
| 55 |
+
if "workflow_id" in incident_dict and incident_dict["workflow_id"]:
|
| 56 |
+
incident_dict["workflow_id"] = ObjectId(incident_dict["workflow_id"]) if isinstance(incident_dict["workflow_id"], str) else incident_dict["workflow_id"]
|
| 57 |
+
|
| 58 |
+
if "log_id" in incident_dict and incident_dict["log_id"]:
|
| 59 |
+
incident_dict["log_id"] = ObjectId(incident_dict["log_id"]) if isinstance(incident_dict["log_id"], str) else incident_dict["log_id"]
|
| 60 |
+
|
| 61 |
+
return cls(**incident_dict)
|
| 62 |
+
|
| 63 |
+
def save(self):
|
| 64 |
+
"""Save incident to database"""
|
| 65 |
+
incidents_collection = get_incidents_collection()
|
| 66 |
+
incident_dict = self.to_dict()
|
| 67 |
+
|
| 68 |
+
if self._id:
|
| 69 |
+
# Update existing incident
|
| 70 |
+
incident_dict["updated_at"] = datetime.now()
|
| 71 |
+
result = incidents_collection.update_one(
|
| 72 |
+
{"_id": ObjectId(self._id)},
|
| 73 |
+
{"$set": incident_dict}
|
| 74 |
+
)
|
| 75 |
+
return result.modified_count > 0
|
| 76 |
+
else:
|
| 77 |
+
# Insert new incident
|
| 78 |
+
incident_dict["created_at"] = datetime.now()
|
| 79 |
+
incident_dict["updated_at"] = datetime.now()
|
| 80 |
+
result = incidents_collection.insert_one(incident_dict)
|
| 81 |
+
self._id = result.inserted_id
|
| 82 |
+
return result.acknowledged
|
| 83 |
+
|
| 84 |
+
@classmethod
|
| 85 |
+
def find_by_id(cls, incident_id):
|
| 86 |
+
"""Find incident by ID"""
|
| 87 |
+
incidents_collection = get_incidents_collection()
|
| 88 |
+
incident_data = incidents_collection.find_one({"_id": ObjectId(incident_id)})
|
| 89 |
+
if incident_data:
|
| 90 |
+
return cls.from_dict(incident_data)
|
| 91 |
+
return None
|
| 92 |
+
|
| 93 |
+
@classmethod
|
| 94 |
+
def find_by_user(cls, user_id):
|
| 95 |
+
"""Find all incidents for a user"""
|
| 96 |
+
incidents_collection = get_incidents_collection()
|
| 97 |
+
incidents_data = incidents_collection.find({"user_id": ObjectId(user_id)})
|
| 98 |
+
return [cls.from_dict(incident_data) for incident_data in incidents_data]
|
| 99 |
+
|
| 100 |
+
@classmethod
|
| 101 |
+
def find_by_department(cls, department_id):
|
| 102 |
+
"""Find all incidents for a department"""
|
| 103 |
+
incidents_collection = get_incidents_collection()
|
| 104 |
+
incidents_data = incidents_collection.find({"department_id": ObjectId(department_id)})
|
| 105 |
+
return [cls.from_dict(incident_data) for incident_data in incidents_data]
|
| 106 |
+
|
| 107 |
+
@classmethod
|
| 108 |
+
def find_by_workflow(cls, workflow_id):
|
| 109 |
+
"""Find all incidents for a workflow"""
|
| 110 |
+
incidents_collection = get_incidents_collection()
|
| 111 |
+
incidents_data = incidents_collection.find({"workflow_id": ObjectId(workflow_id)})
|
| 112 |
+
return [cls.from_dict(incident_data) for incident_data in incidents_data]
|
| 113 |
+
|
| 114 |
+
@classmethod
|
| 115 |
+
def find_by_date_range(cls, start_date, end_date, department_id=None, user_id=None, workflow_id=None):
|
| 116 |
+
"""Find incidents by date range, optionally filtered by department, user, and/or workflow"""
|
| 117 |
+
incidents_collection = get_incidents_collection()
|
| 118 |
+
query = {
|
| 119 |
+
"incident_date": {
|
| 120 |
+
"$gte": start_date,
|
| 121 |
+
"$lte": end_date
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
if department_id:
|
| 126 |
+
query["department_id"] = ObjectId(department_id)
|
| 127 |
+
|
| 128 |
+
if user_id:
|
| 129 |
+
query["user_id"] = ObjectId(user_id)
|
| 130 |
+
|
| 131 |
+
if workflow_id:
|
| 132 |
+
query["workflow_id"] = ObjectId(workflow_id)
|
| 133 |
+
|
| 134 |
+
incidents_data = incidents_collection.find(query)
|
| 135 |
+
return [cls.from_dict(incident_data) for incident_data in incidents_data]
|
| 136 |
+
|
| 137 |
+
@classmethod
|
| 138 |
+
def find_by_log(cls, log_id):
|
| 139 |
+
"""Find all incidents for a log"""
|
| 140 |
+
incidents_collection = get_incidents_collection()
|
| 141 |
+
incidents_data = incidents_collection.find({"log_id": ObjectId(log_id)})
|
| 142 |
+
return [cls.from_dict(incident_data) for incident_data in incidents_data]
|
| 143 |
+
|
| 144 |
+
def delete(self):
|
| 145 |
+
"""Delete incident from database"""
|
| 146 |
+
if not self._id:
|
| 147 |
+
return False
|
| 148 |
+
|
| 149 |
+
incidents_collection = get_incidents_collection()
|
| 150 |
+
result = incidents_collection.delete_one({"_id": ObjectId(self._id)})
|
| 151 |
+
return result.deleted_count > 0
|
| 152 |
+
|
| 153 |
+
def update_processing_status(self, status):
|
| 154 |
+
"""Update incident processing status"""
|
| 155 |
+
self.processing_status = status
|
| 156 |
+
return self.save()
|
| 157 |
+
|
| 158 |
+
def add_filled_form(self, form_path):
|
| 159 |
+
"""Add a filled form to incident"""
|
| 160 |
+
if form_path not in self.filled_forms:
|
| 161 |
+
self.filled_forms.append(form_path)
|
| 162 |
+
return self.save()
|
| 163 |
+
return True
|
| 164 |
+
|
| 165 |
+
def update_form_data(self, form_data):
|
| 166 |
+
"""Update form data"""
|
| 167 |
+
self.form_data = form_data
|
| 168 |
+
return self.save()
|
models/log.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from bson import ObjectId
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from db import get_logs_collection
|
| 4 |
+
|
| 5 |
+
class Log:
|
| 6 |
+
def __init__(self, user_id, department_id, log_date, log_file_path, incidents=None,
|
| 7 |
+
_id=None, created_at=None, updated_at=None, processing_status="pending",
|
| 8 |
+
extracted_text=None, extracted_activities=None):
|
| 9 |
+
self.user_id = user_id
|
| 10 |
+
self.department_id = department_id
|
| 11 |
+
self.log_date = log_date
|
| 12 |
+
self.log_file_path = log_file_path
|
| 13 |
+
self.incidents = incidents or []
|
| 14 |
+
self._id = _id
|
| 15 |
+
self.created_at = created_at or datetime.now()
|
| 16 |
+
self.updated_at = updated_at or datetime.now()
|
| 17 |
+
self.processing_status = processing_status # "pending", "processing", "completed", "failed"
|
| 18 |
+
self.extracted_text = extracted_text # OCR extracted text from PDF
|
| 19 |
+
self.extracted_activities = extracted_activities or [] # Activities extracted by LLM
|
| 20 |
+
|
| 21 |
+
def to_dict(self):
|
| 22 |
+
"""Convert instance to dictionary"""
|
| 23 |
+
log_dict = {
|
| 24 |
+
"user_id": str(self.user_id) if self.user_id else None,
|
| 25 |
+
"department_id": str(self.department_id) if self.department_id else None,
|
| 26 |
+
"log_date": self.log_date,
|
| 27 |
+
"log_file_path": self.log_file_path,
|
| 28 |
+
"incidents": [str(incident_id) for incident_id in self.incidents],
|
| 29 |
+
"created_at": self.created_at,
|
| 30 |
+
"updated_at": self.updated_at,
|
| 31 |
+
"processing_status": self.processing_status,
|
| 32 |
+
"extracted_text": self.extracted_text,
|
| 33 |
+
"extracted_activities": self.extracted_activities
|
| 34 |
+
}
|
| 35 |
+
if self._id:
|
| 36 |
+
log_dict["_id"] = str(self._id)
|
| 37 |
+
return log_dict
|
| 38 |
+
|
| 39 |
+
@classmethod
|
| 40 |
+
def from_dict(cls, log_dict):
|
| 41 |
+
"""Create instance from dictionary"""
|
| 42 |
+
if "_id" in log_dict and log_dict["_id"]:
|
| 43 |
+
log_dict["_id"] = ObjectId(log_dict["_id"]) if isinstance(log_dict["_id"], str) else log_dict["_id"]
|
| 44 |
+
|
| 45 |
+
if "user_id" in log_dict and log_dict["user_id"]:
|
| 46 |
+
log_dict["user_id"] = ObjectId(log_dict["user_id"]) if isinstance(log_dict["user_id"], str) else log_dict["user_id"]
|
| 47 |
+
|
| 48 |
+
if "department_id" in log_dict and log_dict["department_id"]:
|
| 49 |
+
log_dict["department_id"] = ObjectId(log_dict["department_id"]) if isinstance(log_dict["department_id"], str) else log_dict["department_id"]
|
| 50 |
+
|
| 51 |
+
# Convert string IDs to ObjectIds for incidents
|
| 52 |
+
if "incidents" in log_dict and log_dict["incidents"]:
|
| 53 |
+
log_dict["incidents"] = [ObjectId(incident_id) if isinstance(incident_id, str) else incident_id for incident_id in log_dict["incidents"]]
|
| 54 |
+
|
| 55 |
+
return cls(**log_dict)
|
| 56 |
+
|
| 57 |
+
def save(self):
|
| 58 |
+
"""Save log to database"""
|
| 59 |
+
logs_collection = get_logs_collection()
|
| 60 |
+
log_dict = self.to_dict()
|
| 61 |
+
|
| 62 |
+
if self._id:
|
| 63 |
+
# Update existing log
|
| 64 |
+
log_dict["updated_at"] = datetime.now()
|
| 65 |
+
result = logs_collection.update_one(
|
| 66 |
+
{"_id": ObjectId(self._id)},
|
| 67 |
+
{"$set": log_dict}
|
| 68 |
+
)
|
| 69 |
+
return result.modified_count > 0
|
| 70 |
+
else:
|
| 71 |
+
# Insert new log
|
| 72 |
+
log_dict["created_at"] = datetime.now()
|
| 73 |
+
log_dict["updated_at"] = datetime.now()
|
| 74 |
+
result = logs_collection.insert_one(log_dict)
|
| 75 |
+
self._id = result.inserted_id
|
| 76 |
+
return result.acknowledged
|
| 77 |
+
|
| 78 |
+
@classmethod
|
| 79 |
+
def find_by_id(cls, log_id):
|
| 80 |
+
"""Find log by ID"""
|
| 81 |
+
logs_collection = get_logs_collection()
|
| 82 |
+
log_data = logs_collection.find_one({"_id": ObjectId(log_id)})
|
| 83 |
+
if log_data:
|
| 84 |
+
return cls.from_dict(log_data)
|
| 85 |
+
return None
|
| 86 |
+
|
| 87 |
+
@classmethod
|
| 88 |
+
def find_by_user(cls, user_id):
|
| 89 |
+
"""Find all logs for a user"""
|
| 90 |
+
logs_collection = get_logs_collection()
|
| 91 |
+
logs_data = logs_collection.find({"user_id": ObjectId(user_id)})
|
| 92 |
+
return [cls.from_dict(log_data) for log_data in logs_data]
|
| 93 |
+
|
| 94 |
+
@classmethod
|
| 95 |
+
def find_by_department(cls, department_id):
|
| 96 |
+
"""Find all logs for a department"""
|
| 97 |
+
logs_collection = get_logs_collection()
|
| 98 |
+
logs_data = logs_collection.find({"department_id": ObjectId(department_id)})
|
| 99 |
+
return [cls.from_dict(log_data) for log_data in logs_data]
|
| 100 |
+
|
| 101 |
+
@classmethod
|
| 102 |
+
def find_by_date_range(cls, start_date, end_date, department_id=None, user_id=None):
|
| 103 |
+
"""Find logs by date range, optionally filtered by department and/or user"""
|
| 104 |
+
logs_collection = get_logs_collection()
|
| 105 |
+
query = {
|
| 106 |
+
"log_date": {
|
| 107 |
+
"$gte": start_date,
|
| 108 |
+
"$lte": end_date
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
if department_id:
|
| 113 |
+
query["department_id"] = ObjectId(department_id)
|
| 114 |
+
|
| 115 |
+
if user_id:
|
| 116 |
+
query["user_id"] = ObjectId(user_id)
|
| 117 |
+
|
| 118 |
+
logs_data = logs_collection.find(query)
|
| 119 |
+
return [cls.from_dict(log_data) for log_data in logs_data]
|
| 120 |
+
|
| 121 |
+
def delete(self):
|
| 122 |
+
"""Delete log from database"""
|
| 123 |
+
if not self._id:
|
| 124 |
+
return False
|
| 125 |
+
|
| 126 |
+
logs_collection = get_logs_collection()
|
| 127 |
+
result = logs_collection.delete_one({"_id": ObjectId(self._id)})
|
| 128 |
+
return result.deleted_count > 0
|
| 129 |
+
|
| 130 |
+
def update_processing_status(self, status):
|
| 131 |
+
"""Update log processing status"""
|
| 132 |
+
self.processing_status = status
|
| 133 |
+
return self.save()
|
| 134 |
+
|
| 135 |
+
def set_extracted_text(self, text):
|
| 136 |
+
"""Set extracted text from OCR"""
|
| 137 |
+
self.extracted_text = text
|
| 138 |
+
return self.save()
|
| 139 |
+
|
| 140 |
+
def set_extracted_activities(self, activities):
|
| 141 |
+
"""Set extracted activities from LLM"""
|
| 142 |
+
self.extracted_activities = activities
|
| 143 |
+
return self.save()
|
| 144 |
+
|
| 145 |
+
def add_incident(self, incident_id):
|
| 146 |
+
"""Add an incident to log"""
|
| 147 |
+
if incident_id not in self.incidents:
|
| 148 |
+
self.incidents.append(ObjectId(incident_id))
|
| 149 |
+
return self.save()
|
| 150 |
+
return True
|
models/user.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import bcrypt
|
| 2 |
+
from bson import ObjectId
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from db import get_users_collection
|
| 5 |
+
|
| 6 |
+
class User:
|
| 7 |
+
def __init__(self, email, name, password=None, permissions="User", position="Officer",
|
| 8 |
+
department_id=None, logs=None, incidents=None, _id=None, created_at=None, updated_at=None):
|
| 9 |
+
self.email = email
|
| 10 |
+
self.name = name
|
| 11 |
+
self.password = password
|
| 12 |
+
self.permissions = permissions # "Admin" or "User"
|
| 13 |
+
self.position = position
|
| 14 |
+
self.department_id = department_id
|
| 15 |
+
self.logs = logs or []
|
| 16 |
+
self.incidents = incidents or []
|
| 17 |
+
self._id = _id
|
| 18 |
+
self.created_at = created_at or datetime.now()
|
| 19 |
+
self.updated_at = updated_at or datetime.now()
|
| 20 |
+
|
| 21 |
+
@staticmethod
|
| 22 |
+
def hash_password(password):
|
| 23 |
+
"""Hash a password for storing."""
|
| 24 |
+
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
| 25 |
+
|
| 26 |
+
@staticmethod
|
| 27 |
+
def verify_password(stored_password, provided_password):
|
| 28 |
+
"""Verify a stored password against one provided by user"""
|
| 29 |
+
return bcrypt.checkpw(provided_password.encode('utf-8'), stored_password.encode('utf-8'))
|
| 30 |
+
|
| 31 |
+
def to_dict(self):
|
| 32 |
+
"""Convert instance to dictionary (excluding password)"""
|
| 33 |
+
user_dict = {
|
| 34 |
+
"email": self.email,
|
| 35 |
+
"name": self.name,
|
| 36 |
+
"permissions": self.permissions,
|
| 37 |
+
"position": self.position,
|
| 38 |
+
"department_id": str(self.department_id) if self.department_id else None,
|
| 39 |
+
"logs": [str(log_id) for log_id in self.logs],
|
| 40 |
+
"incidents": [str(incident_id) for incident_id in self.incidents],
|
| 41 |
+
"created_at": self.created_at,
|
| 42 |
+
"updated_at": self.updated_at
|
| 43 |
+
}
|
| 44 |
+
if self._id:
|
| 45 |
+
user_dict["_id"] = str(self._id)
|
| 46 |
+
return user_dict
|
| 47 |
+
|
| 48 |
+
@classmethod
|
| 49 |
+
def from_dict(cls, user_dict):
|
| 50 |
+
"""Create instance from dictionary"""
|
| 51 |
+
if "_id" in user_dict and user_dict["_id"]:
|
| 52 |
+
user_dict["_id"] = ObjectId(user_dict["_id"]) if isinstance(user_dict["_id"], str) else user_dict["_id"]
|
| 53 |
+
|
| 54 |
+
if "department_id" in user_dict and user_dict["department_id"]:
|
| 55 |
+
user_dict["department_id"] = ObjectId(user_dict["department_id"]) if isinstance(user_dict["department_id"], str) else user_dict["department_id"]
|
| 56 |
+
|
| 57 |
+
# Convert string IDs to ObjectIds for logs and incidents
|
| 58 |
+
if "logs" in user_dict and user_dict["logs"]:
|
| 59 |
+
user_dict["logs"] = [ObjectId(log_id) if isinstance(log_id, str) else log_id for log_id in user_dict["logs"]]
|
| 60 |
+
|
| 61 |
+
if "incidents" in user_dict and user_dict["incidents"]:
|
| 62 |
+
user_dict["incidents"] = [ObjectId(incident_id) if isinstance(incident_id, str) else incident_id for incident_id in user_dict["incidents"]]
|
| 63 |
+
|
| 64 |
+
return cls(**user_dict)
|
| 65 |
+
|
| 66 |
+
def save(self):
|
| 67 |
+
"""Save user to database"""
|
| 68 |
+
users_collection = get_users_collection()
|
| 69 |
+
user_dict = self.to_dict()
|
| 70 |
+
|
| 71 |
+
# Add password for database storage
|
| 72 |
+
if self.password:
|
| 73 |
+
user_dict["password"] = self.password
|
| 74 |
+
|
| 75 |
+
if self._id:
|
| 76 |
+
# Update existing user
|
| 77 |
+
user_dict["updated_at"] = datetime.now()
|
| 78 |
+
result = users_collection.update_one(
|
| 79 |
+
{"_id": ObjectId(self._id)},
|
| 80 |
+
{"$set": user_dict}
|
| 81 |
+
)
|
| 82 |
+
return result.modified_count > 0
|
| 83 |
+
else:
|
| 84 |
+
# Insert new user
|
| 85 |
+
user_dict["created_at"] = datetime.now()
|
| 86 |
+
user_dict["updated_at"] = datetime.now()
|
| 87 |
+
result = users_collection.insert_one(user_dict)
|
| 88 |
+
self._id = result.inserted_id
|
| 89 |
+
return result.acknowledged
|
| 90 |
+
|
| 91 |
+
@classmethod
|
| 92 |
+
def find_by_id(cls, user_id):
|
| 93 |
+
"""Find user by ID"""
|
| 94 |
+
users_collection = get_users_collection()
|
| 95 |
+
user_data = users_collection.find_one({"_id": ObjectId(user_id)})
|
| 96 |
+
if user_data:
|
| 97 |
+
return cls.from_dict(user_data)
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
@classmethod
|
| 101 |
+
def find_by_email(cls, email):
|
| 102 |
+
"""Find user by email"""
|
| 103 |
+
users_collection = get_users_collection()
|
| 104 |
+
user_data = users_collection.find_one({"email": email})
|
| 105 |
+
if user_data:
|
| 106 |
+
return cls.from_dict(user_data)
|
| 107 |
+
return None
|
| 108 |
+
|
| 109 |
+
@classmethod
|
| 110 |
+
def find_by_department(cls, department_id):
|
| 111 |
+
"""Find all users in a department"""
|
| 112 |
+
users_collection = get_users_collection()
|
| 113 |
+
users_data = users_collection.find({"department_id": ObjectId(department_id)})
|
| 114 |
+
return [cls.from_dict(user_data) for user_data in users_data]
|
| 115 |
+
|
| 116 |
+
def delete(self):
|
| 117 |
+
"""Delete user from database"""
|
| 118 |
+
if not self._id:
|
| 119 |
+
return False
|
| 120 |
+
|
| 121 |
+
users_collection = get_users_collection()
|
| 122 |
+
result = users_collection.delete_one({"_id": ObjectId(self._id)})
|
| 123 |
+
return result.deleted_count > 0
|
| 124 |
+
|
| 125 |
+
def add_log(self, log_id):
|
| 126 |
+
"""Add a log to user's logs"""
|
| 127 |
+
if log_id not in self.logs:
|
| 128 |
+
self.logs.append(ObjectId(log_id))
|
| 129 |
+
return self.save()
|
| 130 |
+
return True
|
| 131 |
+
|
| 132 |
+
def add_incident(self, incident_id):
|
| 133 |
+
"""Add an incident to user's incidents"""
|
| 134 |
+
if incident_id not in self.incidents:
|
| 135 |
+
self.incidents.append(ObjectId(incident_id))
|
| 136 |
+
return self.save()
|
| 137 |
+
return True
|
models/workflow.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from bson import ObjectId
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from db import get_workflows_collection
|
| 4 |
+
|
| 5 |
+
class Workflow:
|
| 6 |
+
def __init__(self, title, description, data_requirements=None, raw_forms=None, form_fields=None,
|
| 7 |
+
department_id=None, _id=None, created_at=None, updated_at=None):
|
| 8 |
+
self.title = title
|
| 9 |
+
self.description = description
|
| 10 |
+
self.data_requirements = data_requirements or [] # List of (String, String) for fields and descriptions
|
| 11 |
+
self.raw_forms = raw_forms or [] # List of file paths
|
| 12 |
+
self.form_fields = form_fields or [] # List of (Number, String) for positions and fields
|
| 13 |
+
self.department_id = department_id
|
| 14 |
+
self._id = _id
|
| 15 |
+
self.created_at = created_at or datetime.now()
|
| 16 |
+
self.updated_at = updated_at or datetime.now()
|
| 17 |
+
|
| 18 |
+
def to_dict(self):
|
| 19 |
+
"""Convert instance to dictionary"""
|
| 20 |
+
workflow_dict = {
|
| 21 |
+
"title": self.title,
|
| 22 |
+
"description": self.description,
|
| 23 |
+
"data_requirements": self.data_requirements,
|
| 24 |
+
"raw_forms": self.raw_forms,
|
| 25 |
+
"form_fields": self.form_fields,
|
| 26 |
+
"department_id": str(self.department_id) if self.department_id else None,
|
| 27 |
+
"created_at": self.created_at,
|
| 28 |
+
"updated_at": self.updated_at
|
| 29 |
+
}
|
| 30 |
+
if self._id:
|
| 31 |
+
workflow_dict["_id"] = str(self._id)
|
| 32 |
+
return workflow_dict
|
| 33 |
+
|
| 34 |
+
@classmethod
|
| 35 |
+
def from_dict(cls, workflow_dict):
|
| 36 |
+
"""Create instance from dictionary"""
|
| 37 |
+
if "_id" in workflow_dict and workflow_dict["_id"]:
|
| 38 |
+
workflow_dict["_id"] = ObjectId(workflow_dict["_id"]) if isinstance(workflow_dict["_id"], str) else workflow_dict["_id"]
|
| 39 |
+
|
| 40 |
+
if "department_id" in workflow_dict and workflow_dict["department_id"]:
|
| 41 |
+
workflow_dict["department_id"] = ObjectId(workflow_dict["department_id"]) if isinstance(workflow_dict["department_id"], str) else workflow_dict["department_id"]
|
| 42 |
+
|
| 43 |
+
return cls(**workflow_dict)
|
| 44 |
+
|
| 45 |
+
def save(self):
|
| 46 |
+
"""Save workflow to database"""
|
| 47 |
+
workflows_collection = get_workflows_collection()
|
| 48 |
+
workflow_dict = self.to_dict()
|
| 49 |
+
|
| 50 |
+
if self._id:
|
| 51 |
+
# Update existing workflow
|
| 52 |
+
workflow_dict["updated_at"] = datetime.now()
|
| 53 |
+
result = workflows_collection.update_one(
|
| 54 |
+
{"_id": ObjectId(self._id)},
|
| 55 |
+
{"$set": workflow_dict}
|
| 56 |
+
)
|
| 57 |
+
return result.modified_count > 0
|
| 58 |
+
else:
|
| 59 |
+
# Insert new workflow
|
| 60 |
+
workflow_dict["created_at"] = datetime.now()
|
| 61 |
+
workflow_dict["updated_at"] = datetime.now()
|
| 62 |
+
result = workflows_collection.insert_one(workflow_dict)
|
| 63 |
+
self._id = result.inserted_id
|
| 64 |
+
return result.acknowledged
|
| 65 |
+
|
| 66 |
+
@classmethod
|
| 67 |
+
def find_by_id(cls, workflow_id):
|
| 68 |
+
"""Find workflow by ID"""
|
| 69 |
+
workflows_collection = get_workflows_collection()
|
| 70 |
+
workflow_data = workflows_collection.find_one({"_id": ObjectId(workflow_id)})
|
| 71 |
+
if workflow_data:
|
| 72 |
+
return cls.from_dict(workflow_data)
|
| 73 |
+
return None
|
| 74 |
+
|
| 75 |
+
@classmethod
|
| 76 |
+
def find_by_title(cls, title, department_id=None):
|
| 77 |
+
"""Find workflow by title, optionally filtered by department"""
|
| 78 |
+
workflows_collection = get_workflows_collection()
|
| 79 |
+
query = {"title": title}
|
| 80 |
+
if department_id:
|
| 81 |
+
query["department_id"] = ObjectId(department_id)
|
| 82 |
+
|
| 83 |
+
workflow_data = workflows_collection.find_one(query)
|
| 84 |
+
if workflow_data:
|
| 85 |
+
return cls.from_dict(workflow_data)
|
| 86 |
+
return None
|
| 87 |
+
|
| 88 |
+
@classmethod
|
| 89 |
+
def find_by_department(cls, department_id):
|
| 90 |
+
"""Find all workflows for a department"""
|
| 91 |
+
workflows_collection = get_workflows_collection()
|
| 92 |
+
workflows_data = workflows_collection.find({"department_id": ObjectId(department_id)})
|
| 93 |
+
return [cls.from_dict(workflow_data) for workflow_data in workflows_data]
|
| 94 |
+
|
| 95 |
+
@classmethod
|
| 96 |
+
def get_all(cls):
|
| 97 |
+
"""Get all workflows"""
|
| 98 |
+
workflows_collection = get_workflows_collection()
|
| 99 |
+
workflows_data = workflows_collection.find()
|
| 100 |
+
return [cls.from_dict(workflow_data) for workflow_data in workflows_data]
|
| 101 |
+
|
| 102 |
+
def delete(self):
|
| 103 |
+
"""Delete workflow from database"""
|
| 104 |
+
if not self._id:
|
| 105 |
+
return False
|
| 106 |
+
|
| 107 |
+
workflows_collection = get_workflows_collection()
|
| 108 |
+
result = workflows_collection.delete_one({"_id": ObjectId(self._id)})
|
| 109 |
+
return result.deleted_count > 0
|
| 110 |
+
|
| 111 |
+
def add_form(self, form_path):
|
| 112 |
+
"""Add a form to workflow"""
|
| 113 |
+
if form_path not in self.raw_forms:
|
| 114 |
+
self.raw_forms.append(form_path)
|
| 115 |
+
return self.save()
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
def remove_form(self, form_path):
|
| 119 |
+
"""Remove a form from workflow"""
|
| 120 |
+
if form_path in self.raw_forms:
|
| 121 |
+
self.raw_forms.remove(form_path)
|
| 122 |
+
return self.save()
|
| 123 |
+
return True
|
| 124 |
+
|
| 125 |
+
def add_data_requirement(self, field, description):
|
| 126 |
+
"""Add a data requirement (field and description)"""
|
| 127 |
+
requirement = (field, description)
|
| 128 |
+
if requirement not in self.data_requirements:
|
| 129 |
+
self.data_requirements.append(requirement)
|
| 130 |
+
return self.save()
|
| 131 |
+
return True
|
| 132 |
+
|
| 133 |
+
def add_form_field(self, position, field_name):
|
| 134 |
+
"""Add a form field (position and field name)"""
|
| 135 |
+
form_field = (position, field_name)
|
| 136 |
+
if form_field not in self.form_fields:
|
| 137 |
+
self.form_fields.append(form_field)
|
| 138 |
+
return self.save()
|
| 139 |
+
return True
|
requirements.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask==2.2.3
|
| 2 |
+
python-dotenv==1.0.0
|
| 3 |
+
pymongo==4.5.0
|
| 4 |
+
flask-cors==4.0.0
|
| 5 |
+
bcrypt==4.0.1
|
| 6 |
+
PyJWT==2.8.0
|
| 7 |
+
gunicorn==21.2.0
|
| 8 |
+
cloudinary==1.35.0
|
| 9 |
+
openai==1.6.1
|
| 10 |
+
pytesseract==0.3.10
|
| 11 |
+
Pillow==10.1.0
|
| 12 |
+
python-magic==0.4.27
|
| 13 |
+
Flask-RESTful==0.3.10
|
| 14 |
+
Werkzeug==2.2.3
|
| 15 |
+
celery==5.3.4
|
| 16 |
+
redis==5.0.1
|
routes/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Routes package initialization
|
routes/department_routes.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint
|
| 2 |
+
from controllers.department_controller import (
|
| 3 |
+
create_department, get_department, update_department,
|
| 4 |
+
delete_department, get_all_departments, add_members_csv,
|
| 5 |
+
add_member, get_department_members, remove_member, update_member_permissions
|
| 6 |
+
)
|
| 7 |
+
from utils.auth import token_required, admin_required
|
| 8 |
+
|
| 9 |
+
# Create blueprint
|
| 10 |
+
department_bp = Blueprint('departments', __name__)
|
| 11 |
+
|
| 12 |
+
# Public route for creating a new department with first admin
|
| 13 |
+
department_bp.route('/', methods=['POST'])(create_department)
|
| 14 |
+
|
| 15 |
+
# Routes that require authentication
|
| 16 |
+
department_bp.route('/', methods=['GET'])(token_required(get_all_departments))
|
| 17 |
+
department_bp.route('/<department_id>', methods=['GET'])(token_required(get_department))
|
| 18 |
+
|
| 19 |
+
# Routes that require admin permissions
|
| 20 |
+
department_bp.route('/<department_id>', methods=['PUT'])(admin_required(update_department))
|
| 21 |
+
department_bp.route('/<department_id>', methods=['DELETE'])(admin_required(delete_department))
|
| 22 |
+
|
| 23 |
+
# Member management routes (admin only)
|
| 24 |
+
department_bp.route('/<department_id>/members', methods=['GET'])(token_required(get_department_members))
|
| 25 |
+
department_bp.route('/<department_id>/members', methods=['POST'])(admin_required(add_member))
|
| 26 |
+
department_bp.route('/<department_id>/members/csv', methods=['POST'])(admin_required(add_members_csv))
|
| 27 |
+
department_bp.route('/<department_id>/members/<user_id>', methods=['DELETE'])(admin_required(remove_member))
|
| 28 |
+
department_bp.route('/<department_id>/members/<user_id>/permissions', methods=['PUT'])(admin_required(update_member_permissions))
|
setup_env.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Setup environment variables for local development.
|
| 3 |
+
This script creates a .env file with the required environment variables.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
|
| 9 |
+
def create_env_file():
|
| 10 |
+
"""Create a .env file with required environment variables."""
|
| 11 |
+
# Define environment variables
|
| 12 |
+
env_vars = {
|
| 13 |
+
"MONGO_URI": "mongodb+srv://dg:123@enflow.9w95qq5.mongodb.net/?retryWrites=true&w=majority&appName=enflow",
|
| 14 |
+
"JWT_SECRET": "fde5be3fca2f594bb39592a9d3781f5dabd1226bfea2d6fc1a51d65fdf031d23b05d4c8e46feb01531c9be214411bbec1523f735139db8f6d15320974e2d5ef3503db53ca1502a0f9489d310f55100baabcf6ebb3b10e975f3b445f70dee4c8c2bced6618ecf0f13ec4029914e05935bc02e7849a55348899fbba06bf6882ef1fcf67e33ce15b8afc08fc81d60868792f69f2a407301bb2f421655dfd8bbfe3481b86fe5ff01f02b30de35df5a35ec3c58a9b7a93b0baddded92c453a06fc5a2bd72ae739ee4ddc785fce6399dfe97f74945ef06023a64cb173800dbd85f10ba8847f9f8391422683d365bfdcabf7949d70a54f919c304463d1448820d6aa1dc",
|
| 15 |
+
"CLOUDINARY_CLOUD_NAME": "djt4gxy9s",
|
| 16 |
+
"CLOUDINARY_API_KEY": "551626585336911",
|
| 17 |
+
"CLOUDINARY_API_SECRET": "d6HCnsoaDBypM1dXCReFoJqkZDA",
|
| 18 |
+
"OPENAI_API_KEY": "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",
|
| 19 |
+
"FLASK_ENV": "development"
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
# Check if .env file already exists
|
| 23 |
+
if os.path.exists('.env'):
|
| 24 |
+
print("Warning: .env file already exists.")
|
| 25 |
+
response = input("Do you want to overwrite it? (y/n): ")
|
| 26 |
+
if response.lower() != 'y':
|
| 27 |
+
print("Operation canceled.")
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
+
# Write environment variables to .env file
|
| 31 |
+
with open('.env', 'w') as f:
|
| 32 |
+
for key, value in env_vars.items():
|
| 33 |
+
f.write(f"{key}={value}\n")
|
| 34 |
+
|
| 35 |
+
print(".env file created successfully.")
|
| 36 |
+
|
| 37 |
+
if __name__ == "__main__":
|
| 38 |
+
print("Setting up environment variables for Enflow Backend...")
|
| 39 |
+
create_env_file()
|
| 40 |
+
print("Setup complete. You can now run the application.")
|
| 41 |
+
print("To start the application, run: python app.py")
|
test_department.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
import json
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
# Load environment variables
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
# Base URL for API
|
| 10 |
+
BASE_URL = "http://localhost:5000/api"
|
| 11 |
+
|
| 12 |
+
def test_create_department():
|
| 13 |
+
"""Test creating a new department with an admin user"""
|
| 14 |
+
# Department data
|
| 15 |
+
department_data = {
|
| 16 |
+
"name": "Test Police Department",
|
| 17 |
+
"address": "123 Test Street, Test City, TS 12345",
|
| 18 |
+
"website": "https://test-pd.example.com",
|
| 19 |
+
"admin_email": "admin@test-pd.example.com",
|
| 20 |
+
"admin_name": "Admin User",
|
| 21 |
+
"admin_password": "SecurePassword123"
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
# Make POST request to create department
|
| 25 |
+
response = requests.post(f"{BASE_URL}/departments", json=department_data)
|
| 26 |
+
|
| 27 |
+
# Print response details
|
| 28 |
+
print(f"Status Code: {response.status_code}")
|
| 29 |
+
print("Response:")
|
| 30 |
+
print(json.dumps(response.json(), indent=2))
|
| 31 |
+
|
| 32 |
+
return response.json()
|
| 33 |
+
|
| 34 |
+
def main():
|
| 35 |
+
"""Run test functions"""
|
| 36 |
+
print("=== Testing Department Creation ===")
|
| 37 |
+
result = test_create_department()
|
| 38 |
+
|
| 39 |
+
# If department was created successfully, store admin details
|
| 40 |
+
if result.get('department') and result.get('admin_user'):
|
| 41 |
+
print("\n=== Department Created Successfully ===")
|
| 42 |
+
print(f"Department Name: {result['department']['name']}")
|
| 43 |
+
print(f"Admin Email: {result['admin_user']['email']}")
|
| 44 |
+
print("You can now use these details to test the auth endpoints")
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
main()
|
utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Utils package initialization
|
utils/auth.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import jwt
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from functools import wraps
|
| 5 |
+
from flask import request, jsonify
|
| 6 |
+
from models.user import User
|
| 7 |
+
|
| 8 |
+
# Secret key for JWT
|
| 9 |
+
SECRET_KEY = os.environ.get('JWT_SECRET')
|
| 10 |
+
|
| 11 |
+
def generate_token(user_id, permissions, expiration_hours=24*30): # 30-day token by default
|
| 12 |
+
"""Generate JWT token for authentication"""
|
| 13 |
+
payload = {
|
| 14 |
+
'user_id': str(user_id),
|
| 15 |
+
'permissions': permissions,
|
| 16 |
+
'exp': datetime.utcnow() + timedelta(hours=expiration_hours)
|
| 17 |
+
}
|
| 18 |
+
return jwt.encode(payload, SECRET_KEY, algorithm='HS256')
|
| 19 |
+
|
| 20 |
+
def decode_token(token):
|
| 21 |
+
"""Decode and validate JWT token"""
|
| 22 |
+
try:
|
| 23 |
+
return jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
|
| 24 |
+
except jwt.ExpiredSignatureError:
|
| 25 |
+
return None
|
| 26 |
+
except jwt.InvalidTokenError:
|
| 27 |
+
return None
|
| 28 |
+
|
| 29 |
+
def token_required(f):
|
| 30 |
+
"""Decorator for routes that require a valid token"""
|
| 31 |
+
@wraps(f)
|
| 32 |
+
def decorated(*args, **kwargs):
|
| 33 |
+
token = None
|
| 34 |
+
auth_header = request.headers.get('Authorization')
|
| 35 |
+
|
| 36 |
+
if auth_header:
|
| 37 |
+
if auth_header.startswith('Bearer '):
|
| 38 |
+
token = auth_header.split(" ")[1]
|
| 39 |
+
|
| 40 |
+
if not token:
|
| 41 |
+
return jsonify({'message': 'Token is missing'}), 401
|
| 42 |
+
|
| 43 |
+
data = decode_token(token)
|
| 44 |
+
if not data:
|
| 45 |
+
return jsonify({'message': 'Token is invalid or expired'}), 401
|
| 46 |
+
|
| 47 |
+
current_user = User.find_by_id(data['user_id'])
|
| 48 |
+
if not current_user:
|
| 49 |
+
return jsonify({'message': 'User not found'}), 401
|
| 50 |
+
|
| 51 |
+
return f(current_user, *args, **kwargs)
|
| 52 |
+
|
| 53 |
+
return decorated
|
| 54 |
+
|
| 55 |
+
def admin_required(f):
|
| 56 |
+
"""Decorator for routes that require admin permissions"""
|
| 57 |
+
@wraps(f)
|
| 58 |
+
def decorated(*args, **kwargs):
|
| 59 |
+
token = None
|
| 60 |
+
auth_header = request.headers.get('Authorization')
|
| 61 |
+
|
| 62 |
+
if auth_header:
|
| 63 |
+
if auth_header.startswith('Bearer '):
|
| 64 |
+
token = auth_header.split(" ")[1]
|
| 65 |
+
|
| 66 |
+
if not token:
|
| 67 |
+
return jsonify({'message': 'Token is missing'}), 401
|
| 68 |
+
|
| 69 |
+
data = decode_token(token)
|
| 70 |
+
if not data:
|
| 71 |
+
return jsonify({'message': 'Token is invalid or expired'}), 401
|
| 72 |
+
|
| 73 |
+
if data['permissions'] != 'Admin':
|
| 74 |
+
return jsonify({'message': 'Admin permissions required'}), 403
|
| 75 |
+
|
| 76 |
+
current_user = User.find_by_id(data['user_id'])
|
| 77 |
+
if not current_user:
|
| 78 |
+
return jsonify({'message': 'User not found'}), 401
|
| 79 |
+
|
| 80 |
+
return f(current_user, *args, **kwargs)
|
| 81 |
+
|
| 82 |
+
return decorated
|