ein-dateien backend erstellt
This commit is contained in:
32
archiv/flask-backend/app/__init__.py
Normal file
32
archiv/flask-backend/app/__init__.py
Normal file
@ -0,0 +1,32 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_cors import CORS
|
||||
from config import Config
|
||||
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
|
||||
def create_app(config_class=Config):
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
CORS(app)
|
||||
|
||||
# Register blueprints
|
||||
from app.api import bp as api_bp
|
||||
app.register_blueprint(api_bp, url_prefix='/api')
|
||||
|
||||
from app.auth import bp as auth_bp
|
||||
app.register_blueprint(auth_bp, url_prefix='/auth')
|
||||
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
return {'status': 'ok'}
|
||||
|
||||
return app
|
||||
|
||||
from app import models
|
5
archiv/flask-backend/app/api/__init__.py
Normal file
5
archiv/flask-backend/app/api/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('api', __name__)
|
||||
|
||||
from app.api import printers, jobs, users
|
219
archiv/flask-backend/app/api/jobs.py
Normal file
219
archiv/flask-backend/app/api/jobs.py
Normal file
@ -0,0 +1,219 @@
|
||||
from flask import request, jsonify
|
||||
from app import db
|
||||
from app.api import bp
|
||||
from app.models import PrintJob, Printer, User
|
||||
from app.auth.routes import token_required, admin_required
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@bp.route('/jobs', methods=['GET'])
|
||||
@token_required
|
||||
def get_jobs():
|
||||
"""Get jobs for the current user or all jobs for admin"""
|
||||
is_admin = request.user_role == 'admin'
|
||||
user_id = request.user_id
|
||||
|
||||
# Parse query parameters
|
||||
status = request.args.get('status') # active, upcoming, completed, aborted, all
|
||||
printer_id = request.args.get('printer_id')
|
||||
|
||||
# Base query
|
||||
query = PrintJob.query
|
||||
|
||||
# Filter by user unless admin
|
||||
if not is_admin:
|
||||
query = query.filter_by(user_id=user_id)
|
||||
|
||||
# Filter by printer if provided
|
||||
if printer_id:
|
||||
query = query.filter_by(printer_id=printer_id)
|
||||
|
||||
# Apply status filter
|
||||
now = datetime.utcnow()
|
||||
if status == 'active':
|
||||
query = query.filter_by(aborted=False) \
|
||||
.filter(PrintJob.start_at <= now) \
|
||||
.filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > now)
|
||||
elif status == 'upcoming':
|
||||
query = query.filter_by(aborted=False) \
|
||||
.filter(PrintJob.start_at > now)
|
||||
elif status == 'completed':
|
||||
query = query.filter_by(aborted=False) \
|
||||
.filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) <= now)
|
||||
elif status == 'aborted':
|
||||
query = query.filter_by(aborted=True)
|
||||
|
||||
# Order by start time, most recent first
|
||||
query = query.order_by(PrintJob.start_at.desc())
|
||||
|
||||
# Execute query
|
||||
jobs = query.all()
|
||||
result = [job.to_dict() for job in jobs]
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/jobs/<job_id>', methods=['GET'])
|
||||
@token_required
|
||||
def get_job(job_id):
|
||||
"""Get a specific job"""
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
|
||||
# Check permissions
|
||||
is_admin = request.user_role == 'admin'
|
||||
user_id = request.user_id
|
||||
|
||||
if not is_admin and job.user_id != user_id:
|
||||
return jsonify({'error': 'Not authorized to view this job'}), 403
|
||||
|
||||
return jsonify(job.to_dict())
|
||||
|
||||
@bp.route('/jobs', methods=['POST'])
|
||||
@token_required
|
||||
def create_job():
|
||||
"""Create a new print job (reserve a printer)"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
required_fields = ['printer_id', 'start_at', 'duration_in_minutes']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return jsonify({'error': f'Missing required field: {field}'}), 400
|
||||
|
||||
# Validate printer
|
||||
printer = Printer.query.get(data['printer_id'])
|
||||
if not printer:
|
||||
return jsonify({'error': 'Printer not found'}), 404
|
||||
|
||||
if printer.status != 0: # Not operational
|
||||
return jsonify({'error': 'Printer is not operational'}), 400
|
||||
|
||||
# Parse start time
|
||||
try:
|
||||
start_at = datetime.fromisoformat(data['start_at'].replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Invalid start_at format'}), 400
|
||||
|
||||
# Validate duration
|
||||
try:
|
||||
duration = int(data['duration_in_minutes'])
|
||||
if duration <= 0 or duration > 480: # Max 8 hours
|
||||
return jsonify({'error': 'Invalid duration (must be between 1 and 480 minutes)'}), 400
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Duration must be a number'}), 400
|
||||
|
||||
end_at = start_at + timedelta(minutes=duration)
|
||||
|
||||
# Check if the printer is available during the requested time
|
||||
conflicting_jobs = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(
|
||||
(PrintJob.start_at < end_at) &
|
||||
(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > start_at)
|
||||
) \
|
||||
.all()
|
||||
|
||||
if conflicting_jobs:
|
||||
return jsonify({'error': 'Printer is not available during the requested time'}), 409
|
||||
|
||||
# Create job
|
||||
job = PrintJob(
|
||||
printer_id=data['printer_id'],
|
||||
user_id=request.user_id,
|
||||
start_at=start_at,
|
||||
duration_in_minutes=duration,
|
||||
comments=data.get('comments', '')
|
||||
)
|
||||
|
||||
db.session.add(job)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(job.to_dict()), 201
|
||||
|
||||
@bp.route('/jobs/<job_id>', methods=['PUT'])
|
||||
@token_required
|
||||
def update_job(job_id):
|
||||
"""Update a job"""
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
|
||||
# Check permissions
|
||||
is_admin = request.user_role == 'admin'
|
||||
user_id = request.user_id
|
||||
|
||||
if not is_admin and job.user_id != user_id:
|
||||
return jsonify({'error': 'Not authorized to update this job'}), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Only allow certain fields to be updated
|
||||
if 'comments' in data:
|
||||
job.comments = data['comments']
|
||||
|
||||
# Admin or owner can abort a job
|
||||
if 'aborted' in data and data['aborted'] and not job.aborted:
|
||||
job.aborted = True
|
||||
job.abort_reason = data.get('abort_reason', '')
|
||||
|
||||
# Admin or owner can extend a job if it's active
|
||||
now = datetime.utcnow()
|
||||
is_active = (not job.aborted and
|
||||
job.start_at <= now and
|
||||
job.get_end_time() > now)
|
||||
|
||||
if 'extend_minutes' in data and is_active:
|
||||
try:
|
||||
extend_minutes = int(data['extend_minutes'])
|
||||
if extend_minutes <= 0 or extend_minutes > 120: # Max extend 2 hours
|
||||
return jsonify({'error': 'Invalid extension (must be between 1 and 120 minutes)'}), 400
|
||||
|
||||
new_end_time = job.get_end_time() + timedelta(minutes=extend_minutes)
|
||||
|
||||
# Check for conflicts with the extension
|
||||
conflicting_jobs = PrintJob.query.filter_by(printer_id=job.printer_id, aborted=False) \
|
||||
.filter(PrintJob.id != job.id) \
|
||||
.filter(PrintJob.start_at < new_end_time) \
|
||||
.filter(PrintJob.start_at > job.get_end_time()) \
|
||||
.all()
|
||||
|
||||
if conflicting_jobs:
|
||||
return jsonify({'error': 'Cannot extend job due to conflicts with other reservations'}), 409
|
||||
|
||||
job.duration_in_minutes += extend_minutes
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Extend minutes must be a number'}), 400
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(job.to_dict())
|
||||
|
||||
@bp.route('/jobs/<job_id>', methods=['DELETE'])
|
||||
@token_required
|
||||
def delete_job(job_id):
|
||||
"""Delete a job (cancel reservation)"""
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
|
||||
# Check permissions
|
||||
is_admin = request.user_role == 'admin'
|
||||
user_id = request.user_id
|
||||
|
||||
if not is_admin and job.user_id != user_id:
|
||||
return jsonify({'error': 'Not authorized to delete this job'}), 403
|
||||
|
||||
# Only allow deletion of upcoming jobs
|
||||
now = datetime.utcnow()
|
||||
if job.start_at <= now and not is_admin:
|
||||
return jsonify({'error': 'Cannot delete an active or completed job'}), 400
|
||||
|
||||
db.session.delete(job)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Job deleted successfully'})
|
||||
|
||||
@bp.route('/jobs/<job_id>/remaining-time', methods=['GET'])
|
||||
def get_remaining_time(job_id):
|
||||
"""Get remaining time for a job (public endpoint)"""
|
||||
job = PrintJob.query.get_or_404(job_id)
|
||||
|
||||
remaining_seconds = job.get_remaining_time()
|
||||
|
||||
return jsonify({
|
||||
'job_id': job.id,
|
||||
'remaining_seconds': remaining_seconds,
|
||||
'is_active': job.is_active()
|
||||
})
|
177
archiv/flask-backend/app/api/printers.py
Normal file
177
archiv/flask-backend/app/api/printers.py
Normal file
@ -0,0 +1,177 @@
|
||||
from flask import request, jsonify
|
||||
from app import db
|
||||
from app.api import bp
|
||||
from app.models import Printer, PrintJob
|
||||
from app.auth.routes import token_required, admin_required
|
||||
from datetime import datetime
|
||||
|
||||
@bp.route('/printers', methods=['GET'])
|
||||
def get_printers():
|
||||
"""Get all printers"""
|
||||
printers = Printer.query.all()
|
||||
result = []
|
||||
|
||||
for printer in printers:
|
||||
# Get active job for the printer if any
|
||||
now = datetime.utcnow()
|
||||
active_job = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(PrintJob.start_at <= now) \
|
||||
.filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > now) \
|
||||
.first()
|
||||
|
||||
printer_data = {
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'description': printer.description,
|
||||
'status': printer.status,
|
||||
'is_available': printer.status == 0 and active_job is None,
|
||||
'active_job': active_job.to_dict() if active_job else None
|
||||
}
|
||||
result.append(printer_data)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/printers/<printer_id>', methods=['GET'])
|
||||
def get_printer(printer_id):
|
||||
"""Get a specific printer"""
|
||||
printer = Printer.query.get_or_404(printer_id)
|
||||
|
||||
# Get active job for the printer if any
|
||||
now = datetime.utcnow()
|
||||
active_job = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(PrintJob.start_at <= now) \
|
||||
.filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > now) \
|
||||
.first()
|
||||
|
||||
# Get upcoming jobs
|
||||
upcoming_jobs = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(PrintJob.start_at > now) \
|
||||
.order_by(PrintJob.start_at) \
|
||||
.limit(5) \
|
||||
.all()
|
||||
|
||||
result = {
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'description': printer.description,
|
||||
'status': printer.status,
|
||||
'is_available': printer.status == 0 and active_job is None,
|
||||
'active_job': active_job.to_dict() if active_job else None,
|
||||
'upcoming_jobs': [job.to_dict() for job in upcoming_jobs]
|
||||
}
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/printers', methods=['POST'])
|
||||
@admin_required
|
||||
def create_printer():
|
||||
"""Create a new printer (admin only)"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
required_fields = ['name', 'description']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return jsonify({'error': f'Missing required field: {field}'}), 400
|
||||
|
||||
printer = Printer(
|
||||
name=data['name'],
|
||||
description=data['description'],
|
||||
status=data.get('status', 0)
|
||||
)
|
||||
|
||||
db.session.add(printer)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'description': printer.description,
|
||||
'status': printer.status
|
||||
}), 201
|
||||
|
||||
@bp.route('/printers/<printer_id>', methods=['PUT'])
|
||||
@admin_required
|
||||
def update_printer(printer_id):
|
||||
"""Update a printer (admin only)"""
|
||||
printer = Printer.query.get_or_404(printer_id)
|
||||
data = request.get_json() or {}
|
||||
|
||||
if 'name' in data:
|
||||
printer.name = data['name']
|
||||
if 'description' in data:
|
||||
printer.description = data['description']
|
||||
if 'status' in data:
|
||||
printer.status = data['status']
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'description': printer.description,
|
||||
'status': printer.status
|
||||
})
|
||||
|
||||
@bp.route('/printers/<printer_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def delete_printer(printer_id):
|
||||
"""Delete a printer (admin only)"""
|
||||
printer = Printer.query.get_or_404(printer_id)
|
||||
|
||||
# Check if the printer has active jobs
|
||||
now = datetime.utcnow()
|
||||
active_jobs = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(PrintJob.start_at <= now) \
|
||||
.filter(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) > now) \
|
||||
.all()
|
||||
|
||||
if active_jobs:
|
||||
return jsonify({'error': 'Cannot delete printer with active jobs'}), 400
|
||||
|
||||
db.session.delete(printer)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Printer deleted successfully'})
|
||||
|
||||
@bp.route('/printers/availability', methods=['GET'])
|
||||
def get_availability():
|
||||
"""Get availability information for all printers"""
|
||||
start_date = request.args.get('start_date')
|
||||
end_date = request.args.get('end_date')
|
||||
|
||||
if not start_date or not end_date:
|
||||
return jsonify({'error': 'start_date and end_date are required'}), 400
|
||||
|
||||
try:
|
||||
start = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
||||
end = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Invalid date format'}), 400
|
||||
|
||||
if start >= end:
|
||||
return jsonify({'error': 'start_date must be before end_date'}), 400
|
||||
|
||||
printers = Printer.query.all()
|
||||
result = []
|
||||
|
||||
for printer in printers:
|
||||
# Get all jobs for this printer in the date range
|
||||
jobs = PrintJob.query.filter_by(printer_id=printer.id, aborted=False) \
|
||||
.filter(
|
||||
(PrintJob.start_at <= end) &
|
||||
(PrintJob.start_at.op('+')(PrintJob.duration_in_minutes * 60) >= start)
|
||||
) \
|
||||
.order_by(PrintJob.start_at) \
|
||||
.all()
|
||||
|
||||
# Convert to availability slots
|
||||
availability = {
|
||||
'printer_id': printer.id,
|
||||
'printer_name': printer.name,
|
||||
'status': printer.status,
|
||||
'jobs': [job.to_dict() for job in jobs]
|
||||
}
|
||||
|
||||
result.append(availability)
|
||||
|
||||
return jsonify(result)
|
139
archiv/flask-backend/app/api/users.py
Normal file
139
archiv/flask-backend/app/api/users.py
Normal file
@ -0,0 +1,139 @@
|
||||
from flask import request, jsonify
|
||||
from app import db
|
||||
from app.api import bp
|
||||
from app.models import User, PrintJob
|
||||
from app.auth.routes import admin_required, token_required
|
||||
|
||||
@bp.route('/users', methods=['GET'])
|
||||
@admin_required
|
||||
def get_users():
|
||||
"""Get all users (admin only)"""
|
||||
users = User.query.all()
|
||||
result = []
|
||||
|
||||
for user in users:
|
||||
# Count jobs
|
||||
total_jobs = PrintJob.query.filter_by(user_id=user.id).count()
|
||||
active_jobs = PrintJob.query.filter_by(user_id=user.id, aborted=False).count()
|
||||
|
||||
user_data = {
|
||||
'id': user.id,
|
||||
'github_id': user.github_id,
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
'email': user.email,
|
||||
'role': user.role,
|
||||
'job_count': total_jobs,
|
||||
'active_job_count': active_jobs
|
||||
}
|
||||
result.append(user_data)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/users/<user_id>', methods=['GET'])
|
||||
@admin_required
|
||||
def get_user(user_id):
|
||||
"""Get a specific user (admin only)"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
# Count jobs
|
||||
total_jobs = PrintJob.query.filter_by(user_id=user.id).count()
|
||||
active_jobs = PrintJob.query.filter_by(user_id=user.id, aborted=False).count()
|
||||
|
||||
result = {
|
||||
'id': user.id,
|
||||
'github_id': user.github_id,
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
'email': user.email,
|
||||
'role': user.role,
|
||||
'job_count': total_jobs,
|
||||
'active_job_count': active_jobs
|
||||
}
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/users/<user_id>', methods=['PUT'])
|
||||
@admin_required
|
||||
def update_user(user_id):
|
||||
"""Update a user (admin only)"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
data = request.get_json() or {}
|
||||
|
||||
if 'role' in data and data['role'] in ['admin', 'user', 'guest']:
|
||||
user.role = data['role']
|
||||
|
||||
if 'display_name' in data:
|
||||
user.display_name = data['display_name']
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'id': user.id,
|
||||
'github_id': user.github_id,
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
'email': user.email,
|
||||
'role': user.role
|
||||
})
|
||||
|
||||
@bp.route('/users/<user_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def delete_user(user_id):
|
||||
"""Delete a user (admin only)"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
# Check if user has active jobs
|
||||
active_jobs = PrintJob.query.filter_by(user_id=user.id, aborted=False).first()
|
||||
if active_jobs:
|
||||
return jsonify({'error': 'Cannot delete user with active jobs'}), 400
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'User deleted successfully'})
|
||||
|
||||
@bp.route('/me', methods=['GET'])
|
||||
@token_required
|
||||
def get_current_user():
|
||||
"""Get the current user's profile"""
|
||||
user = User.query.get(request.user_id)
|
||||
if not user:
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
|
||||
result = {
|
||||
'id': user.id,
|
||||
'github_id': user.github_id,
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
'email': user.email,
|
||||
'role': user.role
|
||||
}
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route('/me', methods=['PUT'])
|
||||
@token_required
|
||||
def update_current_user():
|
||||
"""Update the current user's profile"""
|
||||
user = User.query.get(request.user_id)
|
||||
if not user:
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
if 'display_name' in data:
|
||||
user.display_name = data['display_name']
|
||||
|
||||
db.session.commit()
|
||||
|
||||
result = {
|
||||
'id': user.id,
|
||||
'github_id': user.github_id,
|
||||
'username': user.username,
|
||||
'display_name': user.display_name,
|
||||
'email': user.email,
|
||||
'role': user.role
|
||||
}
|
||||
|
||||
return jsonify(result)
|
5
archiv/flask-backend/app/auth/__init__.py
Normal file
5
archiv/flask-backend/app/auth/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('auth', __name__)
|
||||
|
||||
from app.auth import routes
|
156
archiv/flask-backend/app/auth/routes.py
Normal file
156
archiv/flask-backend/app/auth/routes.py
Normal file
@ -0,0 +1,156 @@
|
||||
from flask import request, jsonify, current_app
|
||||
from app import db
|
||||
from app.auth import bp
|
||||
from app.models import User, Session
|
||||
from datetime import datetime, timedelta
|
||||
import time
|
||||
import functools
|
||||
import re
|
||||
|
||||
@bp.route('/register', methods=['POST'])
|
||||
def register():
|
||||
"""Register a new user"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Validate required fields
|
||||
required_fields = ['username', 'email', 'password']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return jsonify({'error': f'Missing required field: {field}'}), 400
|
||||
|
||||
# Validate email format
|
||||
email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
if not re.match(email_regex, data['email']):
|
||||
return jsonify({'error': 'Invalid email format'}), 400
|
||||
|
||||
# Validate password strength (at least 8 characters)
|
||||
if len(data['password']) < 8:
|
||||
return jsonify({'error': 'Password must be at least 8 characters long'}), 400
|
||||
|
||||
# Check if username already exists
|
||||
if User.query.filter_by(username=data['username']).first():
|
||||
return jsonify({'error': 'Username already exists'}), 400
|
||||
|
||||
# Check if email already exists
|
||||
if User.query.filter_by(email=data['email']).first():
|
||||
return jsonify({'error': 'Email already exists'}), 400
|
||||
|
||||
# Create new user
|
||||
user = User(
|
||||
username=data['username'],
|
||||
email=data['email'],
|
||||
display_name=data.get('display_name', data['username']),
|
||||
role='user' # Default role
|
||||
)
|
||||
user.set_password(data['password'])
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'display_name': user.display_name,
|
||||
'role': user.role
|
||||
}), 201
|
||||
|
||||
@bp.route('/login', methods=['POST'])
|
||||
def login():
|
||||
"""Login a user with username/email and password"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
# Validate required fields
|
||||
if 'password' not in data:
|
||||
return jsonify({'error': 'Password is required'}), 400
|
||||
|
||||
if 'username' not in data and 'email' not in data:
|
||||
return jsonify({'error': 'Username or email is required'}), 400
|
||||
|
||||
# Find user by username or email
|
||||
user = None
|
||||
if 'username' in data:
|
||||
user = User.query.filter_by(username=data['username']).first()
|
||||
else:
|
||||
user = User.query.filter_by(email=data['email']).first()
|
||||
|
||||
# Check if user exists and verify password
|
||||
if not user or not user.check_password(data['password']):
|
||||
return jsonify({'error': 'Invalid credentials'}), 401
|
||||
|
||||
# Create a session for the user
|
||||
expires_at = int((datetime.utcnow() + timedelta(days=7)).timestamp())
|
||||
session = Session(
|
||||
user_id=user.id,
|
||||
expires_at=expires_at
|
||||
)
|
||||
db.session.add(session)
|
||||
db.session.commit()
|
||||
|
||||
# Generate JWT token
|
||||
token = user.generate_token()
|
||||
|
||||
return jsonify({
|
||||
'token': token,
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'display_name': user.display_name,
|
||||
'role': user.role
|
||||
}
|
||||
})
|
||||
|
||||
@bp.route('/logout', methods=['POST'])
|
||||
def logout():
|
||||
"""Log out a user by invalidating their session"""
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return jsonify({'error': 'Authorization header required'}), 401
|
||||
|
||||
token = auth_header.split(' ')[1]
|
||||
payload = User.verify_token(token)
|
||||
if not payload:
|
||||
return jsonify({'error': 'Invalid token'}), 401
|
||||
|
||||
# Delete all sessions for this user
|
||||
Session.query.filter_by(user_id=payload['user_id']).delete()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Successfully logged out'})
|
||||
|
||||
def token_required(f):
|
||||
@functools.wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return jsonify({'error': 'Authorization header required'}), 401
|
||||
|
||||
token = auth_header.split(' ')[1]
|
||||
payload = User.verify_token(token)
|
||||
if not payload:
|
||||
return jsonify({'error': 'Invalid token'}), 401
|
||||
|
||||
# Check if user has an active session
|
||||
user_id = payload['user_id']
|
||||
current_time = int(time.time())
|
||||
session = Session.query.filter_by(user_id=user_id).filter(Session.expires_at > current_time).first()
|
||||
if not session:
|
||||
return jsonify({'error': 'No active session found'}), 401
|
||||
|
||||
# Add user to request context
|
||||
request.user_id = user_id
|
||||
request.user_role = payload['role']
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
def admin_required(f):
|
||||
@functools.wraps(f)
|
||||
@token_required
|
||||
def decorated(*args, **kwargs):
|
||||
if request.user_role != 'admin':
|
||||
return jsonify({'error': 'Admin privileges required'}), 403
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
124
archiv/flask-backend/app/models.py
Normal file
124
archiv/flask-backend/app/models.py
Normal file
@ -0,0 +1,124 @@
|
||||
from app import db
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
import jwt
|
||||
from config import Config
|
||||
import bcrypt
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = 'user'
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
username = db.Column(db.String(64), index=True, unique=True, nullable=False)
|
||||
display_name = db.Column(db.String(120))
|
||||
email = db.Column(db.String(120), index=True, unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(128), nullable=False)
|
||||
role = db.Column(db.String(20), default='user')
|
||||
|
||||
print_jobs = db.relationship('PrintJob', backref='user', lazy='dynamic', cascade='all, delete-orphan')
|
||||
sessions = db.relationship('Session', backref='user', lazy='dynamic', cascade='all, delete-orphan')
|
||||
|
||||
def set_password(self, password):
|
||||
"""Hash and set the user's password"""
|
||||
password_bytes = password.encode('utf-8')
|
||||
salt = bcrypt.gensalt()
|
||||
self.password_hash = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
|
||||
|
||||
def check_password(self, password):
|
||||
"""Check if the provided password matches the stored hash"""
|
||||
password_bytes = password.encode('utf-8')
|
||||
stored_hash = self.password_hash.encode('utf-8')
|
||||
return bcrypt.checkpw(password_bytes, stored_hash)
|
||||
|
||||
def generate_token(self):
|
||||
"""Generate a JWT token for this user"""
|
||||
payload = {
|
||||
'user_id': self.id,
|
||||
'username': self.username,
|
||||
'email': self.email,
|
||||
'role': self.role,
|
||||
'exp': datetime.utcnow() + timedelta(seconds=Config.JWT_ACCESS_TOKEN_EXPIRES)
|
||||
}
|
||||
return jwt.encode(payload, Config.JWT_SECRET, algorithm='HS256')
|
||||
|
||||
@staticmethod
|
||||
def verify_token(token):
|
||||
"""Verify and decode a JWT token"""
|
||||
try:
|
||||
payload = jwt.decode(token, Config.JWT_SECRET, algorithms=['HS256'])
|
||||
return payload
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class Session(db.Model):
|
||||
__tablename__ = 'session'
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = db.Column(db.String(36), db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
|
||||
expires_at = db.Column(db.Integer, nullable=False)
|
||||
|
||||
|
||||
class Printer(db.Model):
|
||||
__tablename__ = 'printer'
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
name = db.Column(db.String(120), nullable=False)
|
||||
description = db.Column(db.Text, nullable=False)
|
||||
status = db.Column(db.Integer, nullable=False, default=0) # 0: OPERATIONAL, 1: OUT_OF_ORDER
|
||||
|
||||
print_jobs = db.relationship('PrintJob', backref='printer', lazy='dynamic', cascade='all, delete-orphan')
|
||||
|
||||
|
||||
class PrintJob(db.Model):
|
||||
__tablename__ = 'printJob'
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
printer_id = db.Column(db.String(36), db.ForeignKey('printer.id', ondelete='CASCADE'), nullable=False)
|
||||
user_id = db.Column(db.String(36), db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
|
||||
start_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
duration_in_minutes = db.Column(db.Integer, nullable=False)
|
||||
comments = db.Column(db.Text)
|
||||
aborted = db.Column(db.Boolean, nullable=False, default=False)
|
||||
abort_reason = db.Column(db.Text)
|
||||
|
||||
def get_end_time(self):
|
||||
return self.start_at + timedelta(minutes=self.duration_in_minutes)
|
||||
|
||||
def is_active(self):
|
||||
now = datetime.utcnow()
|
||||
return (not self.aborted and
|
||||
self.start_at <= now and
|
||||
now < self.get_end_time())
|
||||
|
||||
def get_remaining_time(self):
|
||||
if self.aborted:
|
||||
return 0
|
||||
|
||||
now = datetime.utcnow()
|
||||
if now < self.start_at:
|
||||
# Job hasn't started yet
|
||||
return self.duration_in_minutes * 60
|
||||
|
||||
end_time = self.get_end_time()
|
||||
if now >= end_time:
|
||||
# Job has ended
|
||||
return 0
|
||||
|
||||
# Job is ongoing
|
||||
remaining_seconds = (end_time - now).total_seconds()
|
||||
return int(remaining_seconds)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'printer_id': self.printer_id,
|
||||
'user_id': self.user_id,
|
||||
'start_at': self.start_at.isoformat(),
|
||||
'duration_in_minutes': self.duration_in_minutes,
|
||||
'comments': self.comments,
|
||||
'aborted': self.aborted,
|
||||
'abort_reason': self.abort_reason,
|
||||
'remaining_time': self.get_remaining_time(),
|
||||
'is_active': self.is_active()
|
||||
}
|
Reference in New Issue
Block a user