# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from functools import wraps import os import sqlite3 from dotenv import load_dotenv from flask import Flask from flask import g from flask import jsonify from flask import request from hotelbooker_core import HotelBooker import jwt import requests # Load environment variables from .env file load_dotenv() app = Flask(__name__) # Instantiate the core logic class hotel_booker = HotelBooker() app.config["DATABASE"] = hotel_booker.db_name OIDC_CONFIG_URL = os.environ.get( "OIDC_CONFIG_URL", "http://localhost:5000/.well-known/openid-configuration" ) # Cache for OIDC discovery and JWKS oidc_config = None jwks = None def get_oidc_config(): """Fetches and caches the OIDC configuration.""" global oidc_config if oidc_config is None: try: response = requests.get(OIDC_CONFIG_URL) response.raise_for_status() oidc_config = response.json() except requests.exceptions.RequestException as e: return None, f"Error fetching OIDC config: {e}" return oidc_config, None def get_jwks(): """Fetches and caches the JSON Web Key Set (JWKS).""" global jwks if jwks is None: config, error = get_oidc_config() if error: return None, error jwks_uri = config.get("jwks_uri") if not jwks_uri: return None, "jwks_uri not found in OIDC configuration." try: response = requests.get(jwks_uri) response.raise_for_status() jwks = response.json() except requests.exceptions.RequestException as e: return None, f"Error fetching JWKS: {e}" return jwks, None def get_db(): """Manages a per-request database connection.""" if "db" not in g: g.db = sqlite3.connect(app.config["DATABASE"]) g.db.row_factory = sqlite3.Row return g.db @app.teardown_appcontext def close_db(exception): db = g.pop("db", None) if db is not None: db.close() def is_token_valid(token: str): """ Validates a JWT token using the public key from the OIDC jwks_uri. """ if not token: return False, "Token is empty." jwks_data, error = get_jwks() if error: return False, f"Failed to get JWKS: {error}" try: header = jwt.get_unverified_header(token) kid = header.get("kid") if not kid: return False, "Token header missing 'kid'." key = next( (k for k in jwks_data.get("keys", []) if k.get("kid") == kid), None ) if not key: return False, "No matching key found in JWKS." public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key) # The decoding happens just so that we are able to # check if there were any exception decoding the token # which indicate it being not valid. # Also you could have verify_aud and verify_iss as False # But when they are true issuer and audience are needed in the jwt.decode call # they are checked against the values from the token # ideally token validation should also check whether the API being called is part of # audience so for example localhost:8081/api should cover localhost:8081/api/hotels # but should not cover localhost:8000/admin # so this middleware (decorator - is_token_valid, can check the request url and do that check, but we are # skipping that as the audience will always be localhost:8081) decoded_token = jwt.decode( token, key=public_key, issuer="http://localhost:5000", audience="http://localhost:8081", algorithms=[header["alg"]], options={"verify_exp": True, "verify_aud": True, "verify_iss": True}, ) return True, "Token is valid." except jwt.ExpiredSignatureError: return False, "Token has expired." except jwt.InvalidAudienceError: return False, "Invalid audience." except jwt.InvalidIssuerError: return False, "Invalid issuer." except jwt.InvalidTokenError as e: return False, f"Invalid token: {e}" except Exception as e: return False, f"An unexpected error occurred during token validation: {e}" # Decorator to check for a valid access token on protected routes def token_required(f): @wraps(f) def decorated_function(*args, **kwargs): auth_header = request.headers.get("Authorization") if not auth_header or not auth_header.startswith("Bearer "): return { "error": True, "data": None, "message": "Missing or invalid Authorization header.", }, 401 token = auth_header.split(" ")[1] is_valid, message = is_token_valid(token) if not is_valid: return {"error": True, "data": None, "message": message}, 401 return f(*args, **kwargs) return decorated_function @app.route("/hotels", methods=["GET"]) @token_required def get_hotels(): location = request.args.get("location") hotels, error_message = hotel_booker.get_available_hotels( get_db().cursor(), location ) if hotels is not None: return ( jsonify({ "error": False, "data": hotels, "message": "Successfully retrieved hotels.", }), 200, ) else: return jsonify({"error": True, "data": None, "message": error_message}), 500 @app.route("/book", methods=["POST"]) @token_required def book_room(): conn = get_db() data = request.json hotel_id = data.get("hotel_id") guest_name = data.get("guest_name") check_in_date = data.get("check_in_date") check_out_date = data.get("check_out_date") num_rooms = data.get("num_rooms") if not all([hotel_id, guest_name, check_in_date, check_out_date, num_rooms]): return ( jsonify({ "error": True, "data": None, "message": "Missing required booking information.", }), 400, ) booking_id, error_message = hotel_booker.book_a_room( conn, hotel_id, guest_name, check_in_date, check_out_date, num_rooms ) if booking_id: return ( jsonify({ "error": False, "data": {"booking_id": booking_id}, "message": "Booking successful!", }), 200, ) else: return jsonify({"error": True, "data": None, "message": error_message}), 400 @app.route("/booking_details", methods=["GET"]) @token_required def get_details(): conn = get_db() booking_id = request.args.get("booking_id") guest_name = request.args.get("guest_name") if not booking_id and not guest_name: return ( jsonify({ "error": True, "data": None, "message": "Please provide either a booking ID or a guest name.", }), 400, ) details, error_message = hotel_booker.get_booking_details( get_db().cursor(), booking_id=booking_id, guest_name=guest_name ) if details: return ( jsonify({ "error": False, "data": details, "message": "Booking details retrieved successfully.", }), 200, ) else: return jsonify({"error": True, "data": None, "message": error_message}), 404 if __name__ == "__main__": app.run(debug=True, port=8081)