"""USB plugin API endpoints.""" from flask import Blueprint, request from flask_jwt_extended import jwt_required, get_jwt_identity from datetime import datetime from shopdb.extensions import db from shopdb.utils.responses import ( success_response, error_response, paginated_response, ErrorCodes ) from shopdb.utils.pagination import get_pagination_params, paginate_query from ..models import USBDevice, USBDeviceType, USBCheckout usb_bp = Blueprint('usb', __name__) # ============================================================================= # USB Device Types # ============================================================================= @usb_bp.route('/types', methods=['GET']) @jwt_required(optional=True) def list_device_types(): """List all USB device types.""" types = USBDeviceType.query.filter_by(isactive=True).order_by(USBDeviceType.typename).all() return success_response([{ 'usbdevicetypeid': t.usbdevicetypeid, 'typename': t.typename, 'description': t.description, 'icon': t.icon } for t in types]) @usb_bp.route('/types', methods=['POST']) @jwt_required() def create_device_type(): """Create a new USB device type.""" data = request.get_json() or {} if not data.get('typename'): return error_response(ErrorCodes.VALIDATION_ERROR, 'typename is required') if USBDeviceType.query.filter_by(typename=data['typename']).first(): return error_response(ErrorCodes.CONFLICT, 'Type name already exists', http_code=409) device_type = USBDeviceType( typename=data['typename'], description=data.get('description'), icon=data.get('icon', 'usb') ) db.session.add(device_type) db.session.commit() return success_response({ 'usbdevicetypeid': device_type.usbdevicetypeid, 'typename': device_type.typename }, message='Device type created', http_code=201) # ============================================================================= # USB Devices # ============================================================================= @usb_bp.route('', methods=['GET']) @jwt_required(optional=True) def list_usb_devices(): """ List all USB devices with checkout status. Query parameters: - page, per_page: Pagination - search: Search by serial number, label, or asset number - available: Filter to only available (not checked out) devices - typeid: Filter by device type ID """ page, per_page = get_pagination_params(request) query = USBDevice.query.filter_by(isactive=True) # Filter by type if type_id := request.args.get('typeid'): query = query.filter_by(usbdevicetypeid=int(type_id)) # Filter by checkout status if request.args.get('available', '').lower() == 'true': query = query.filter_by(ischeckedout=False) elif request.args.get('checkedout', '').lower() == 'true': query = query.filter_by(ischeckedout=True) # Search filter if search := request.args.get('search'): query = query.filter( db.or_( USBDevice.serialnumber.ilike(f'%{search}%'), USBDevice.label.ilike(f'%{search}%'), USBDevice.assetnumber.ilike(f'%{search}%'), USBDevice.manufacturer.ilike(f'%{search}%') ) ) query = query.order_by(USBDevice.label, USBDevice.serialnumber) items, total = paginate_query(query, page, per_page) data = [device.to_dict() for device in items] return paginated_response(data, page, per_page, total) @usb_bp.route('', methods=['POST']) @jwt_required() def create_usb_device(): """Create a new USB device.""" data = request.get_json() or {} if not data.get('serialnumber'): return error_response(ErrorCodes.VALIDATION_ERROR, 'serialnumber is required') if USBDevice.query.filter_by(serialnumber=data['serialnumber']).first(): return error_response(ErrorCodes.CONFLICT, 'Serial number already exists', http_code=409) device = USBDevice( serialnumber=data['serialnumber'], label=data.get('label'), assetnumber=data.get('assetnumber'), usbdevicetypeid=data.get('usbdevicetypeid'), capacitygb=data.get('capacitygb'), vendorid=data.get('vendorid'), productid=data.get('productid'), manufacturer=data.get('manufacturer'), productname=data.get('productname'), storagelocation=data.get('storagelocation'), pin=data.get('pin'), notes=data.get('notes'), ischeckedout=False ) db.session.add(device) db.session.commit() return success_response(device.to_dict(), message='Device created', http_code=201) @usb_bp.route('/', methods=['GET']) @jwt_required(optional=True) def get_usb_device(device_id: int): """Get a single USB device with checkout history.""" device = USBDevice.query.filter_by(usbdeviceid=device_id, isactive=True).first() if not device: return error_response( ErrorCodes.NOT_FOUND, f'USB device with ID {device_id} not found', http_code=404 ) # Get recent checkout history checkouts = USBCheckout.query.filter_by( usbdeviceid=device_id ).order_by(USBCheckout.checkout_time.desc()).limit(20).all() result = device.to_dict() result['checkout_history'] = [c.to_dict() for c in checkouts] return success_response(result) @usb_bp.route('/', methods=['PUT']) @jwt_required() def update_usb_device(device_id: int): """Update a USB device.""" device = USBDevice.query.filter_by(usbdeviceid=device_id, isactive=True).first() if not device: return error_response( ErrorCodes.NOT_FOUND, f'USB device with ID {device_id} not found', http_code=404 ) data = request.get_json() or {} # Update allowed fields for field in ['label', 'assetnumber', 'usbdevicetypeid', 'capacitygb', 'vendorid', 'productid', 'manufacturer', 'productname', 'storagelocation', 'pin', 'notes']: if field in data: setattr(device, field, data[field]) device.modifieddate = datetime.utcnow() db.session.commit() return success_response(device.to_dict(), message='Device updated') @usb_bp.route('/', methods=['DELETE']) @jwt_required() def delete_usb_device(device_id: int): """Soft delete a USB device.""" device = USBDevice.query.filter_by(usbdeviceid=device_id, isactive=True).first() if not device: return error_response( ErrorCodes.NOT_FOUND, f'USB device with ID {device_id} not found', http_code=404 ) if device.ischeckedout: return error_response( ErrorCodes.VALIDATION_ERROR, 'Cannot delete a device that is currently checked out', http_code=400 ) device.isactive = False device.modifieddate = datetime.utcnow() db.session.commit() return success_response(None, message='Device deleted') # ============================================================================= # Checkout/Checkin Operations # ============================================================================= @usb_bp.route('//checkout', methods=['POST']) @jwt_required() def checkout_device(device_id: int): """Check out a USB device.""" device = USBDevice.query.filter_by(usbdeviceid=device_id, isactive=True).first() if not device: return error_response( ErrorCodes.NOT_FOUND, f'USB device with ID {device_id} not found', http_code=404 ) if device.ischeckedout: return error_response( ErrorCodes.CONFLICT, f'Device is already checked out to {device.currentusername or device.currentuserid}', http_code=409 ) data = request.get_json() or {} if not data.get('sso'): return error_response(ErrorCodes.VALIDATION_ERROR, 'sso is required') # Create checkout record checkout = USBCheckout( usbdeviceid=device_id, machineid=0, # Legacy field, set to 0 for new checkouts sso=data['sso'], checkout_name=data.get('checkout_name'), checkout_time=datetime.utcnow(), checkout_reason=data.get('checkout_reason'), was_wiped=False ) # Update device status device.ischeckedout = True device.currentuserid = data['sso'] device.currentusername = data.get('checkout_name') device.currentcheckoutdate = datetime.utcnow() device.modifieddate = datetime.utcnow() db.session.add(checkout) db.session.commit() return success_response(checkout.to_dict(), message='Device checked out', http_code=201) @usb_bp.route('//checkin', methods=['POST']) @jwt_required() def checkin_device(device_id: int): """Check in a USB device.""" device = USBDevice.query.filter_by(usbdeviceid=device_id, isactive=True).first() if not device: return error_response( ErrorCodes.NOT_FOUND, f'USB device with ID {device_id} not found', http_code=404 ) if not device.ischeckedout: return error_response( ErrorCodes.VALIDATION_ERROR, 'Device is not currently checked out', http_code=400 ) # Find active checkout active_checkout = USBCheckout.query.filter_by( usbdeviceid=device_id, checkin_time=None ).first() data = request.get_json() or {} if active_checkout: active_checkout.checkin_time = datetime.utcnow() active_checkout.checkin_notes = data.get('checkin_notes', active_checkout.checkin_notes) active_checkout.was_wiped = data.get('was_wiped', False) # Update device status device.ischeckedout = False device.currentuserid = None device.currentusername = None device.currentcheckoutdate = None device.modifieddate = datetime.utcnow() db.session.commit() return success_response( active_checkout.to_dict() if active_checkout else None, message='Device checked in' ) # ============================================================================= # Checkout History # ============================================================================= @usb_bp.route('//history', methods=['GET']) @jwt_required(optional=True) def get_device_history(device_id: int): """Get checkout history for a USB device.""" page, per_page = get_pagination_params(request) query = USBCheckout.query.filter_by( usbdeviceid=device_id ).order_by(USBCheckout.checkout_time.desc()) items, total = paginate_query(query, page, per_page) data = [c.to_dict() for c in items] return paginated_response(data, page, per_page, total) @usb_bp.route('/checkouts', methods=['GET']) @jwt_required(optional=True) def list_all_checkouts(): """ List all checkouts (active and historical). Query parameters: - active: Filter to only active (not returned) checkouts - sso: Filter by user SSO """ page, per_page = get_pagination_params(request) query = USBCheckout.query # Filter by active only if request.args.get('active', '').lower() == 'true': query = query.filter(USBCheckout.checkin_time == None) # Filter by user if sso := request.args.get('sso'): query = query.filter(USBCheckout.sso == sso) query = query.order_by(USBCheckout.checkout_time.desc()) items, total = paginate_query(query, page, per_page) data = [c.to_dict() for c in items] return paginated_response(data, page, per_page, total) @usb_bp.route('/checkouts/active', methods=['GET']) @jwt_required(optional=True) def list_active_checkouts(): """List all currently active checkouts.""" checkouts = USBCheckout.query.filter( USBCheckout.checkin_time == None ).order_by(USBCheckout.checkout_time.desc()).all() return success_response([c.to_dict() for c in checkouts])