File size: 10,096 Bytes
c1a64ad
 
 
 
ecded06
c1a64ad
 
 
 
 
 
ecded06
c1a64ad
 
0a76ba8
c1a64ad
4489084
 
c1a64ad
4489084
 
ecded06
 
 
c1a64ad
 
4489084
 
 
 
 
 
 
 
ecded06
4489084
 
 
 
 
 
ecded06
 
 
4489084
ecded06
 
 
4489084
ecded06
 
4489084
ecded06
 
4489084
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40493b1
ecded06
 
c1a64ad
 
4489084
c1a64ad
ecded06
c1a64ad
40493b1
c1a64ad
 
 
 
4489084
 
c1a64ad
 
 
 
 
d775574
 
 
40493b1
d775574
 
40493b1
 
 
c1a64ad
 
 
 
 
4489084
 
c1a64ad
ecded06
 
4489084
ecded06
40493b1
ecded06
 
 
 
 
4489084
 
ecded06
 
 
 
 
 
 
 
 
 
 
4489084
ecded06
 
 
 
40493b1
ecded06
4489084
 
 
 
40493b1
4489084
 
 
 
 
 
 
81ebcc0
4489084
40493b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c1a64ad
 
4489084
c1a64ad
 
4489084
c1a64ad
 
 
4489084
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c1a64ad
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import gradio as gr
import json
import logging
import tempfile
import core  # Import our core module

# --- Configure Logging ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# ==============================================================================
#  CONFIGURATION & DATA HANDLING
# ==============================================================================

DEFAULT_CONFIG_FILE = "./endpoints.json"

def load_config_from_file(filepath: str) -> list:
    """Loads and validates the JSON configuration from a file."""
    try:
        with open(filepath, "r") as f:
            data = json.load(f)
        if not isinstance(data, list):
            raise TypeError("JSON root must be a list of service objects.")
        for service in data:
            if not all(k in service for k in ("name", "link", "public_key")):
                raise ValueError("Each service object must contain 'name', 'link', and 'public_key' keys.")
        logger.info(f"Successfully loaded {len(data)} services from {filepath}.")
        return data
    except FileNotFoundError:
        logger.warning(f"{filepath} not found. Returning empty list.")
        return []
    except Exception as e:
        logger.error(f"Error loading config file {filepath}: {e}")
        return []

def save_config_to_file(filepath: str, config_data: list) -> str:
    """Saves the current configuration data to a JSON file."""
    try:
        with open(filepath, "w") as f:
            json.dump(config_data, f, indent=2)
        status_msg = f"βœ… Success! Configuration saved to {filepath}."
        logger.info(status_msg)
        return status_msg
    except Exception as e:
        status_msg = f"❌ Error saving configuration: {e}"
        logger.error(status_msg)
        return status_msg

initial_config = load_config_from_file(DEFAULT_CONFIG_FILE)

# ==============================================================================
#  GRADIO UI HELPER FUNCTIONS
# ==============================================================================

def add_new_service(current_config: list, name: str, link: str, public_key: str):
    """Adds a new service to the current configuration state."""
    if not all([name, link, public_key]):
        raise gr.Error("All fields (Name, Link, Public Key) are required to add a new service.")
    
    new_service = {"name": name, "link": link, "public_key": public_key}
    
    if any(service['name'] == name for service in current_config):
        raise gr.Error(f"A service with the name '{name}' already exists. Please use a unique name.")
        
    updated_config = current_config + [new_service]
    updated_choices = [service['name'] for service in updated_config]
    
    status_update = f"βœ… '{name}' added. You can now use it in the 'Create Image' tab. Click 'Save to File' to make it permanent."
    return updated_config, gr.Dropdown(choices=updated_choices), status_update, "", "", ""

def generate_image(selected_service_name: str, secret_data_str: str, current_config: list):
    """The main image creation function for the UI."""
    if not all([selected_service_name, secret_data_str]):
        raise gr.Error("A selected service and secret data are both required.")

    try:
        public_key = next((s['public_key'] for s in current_config if s.get('name') == selected_service_name), None)
        if not public_key:
            raise gr.Error(f"Could not find the service '{selected_service_name}'. Please check the configuration.")

        # This call is correct and should not cause an error
        encrypted_image = core.create_encrypted_image(secret_data_str, public_key)
        
        with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_file:
            encrypted_image.save(tmp_file.name)
            logger.info(f"Generated image saved for '{selected_service_name}'.")
            return encrypted_image, tmp_file.name, f"βœ… Success! Image created for '{selected_service_name}'."

    except Exception as e:
        logger.error(f"Image creation failed: {e}", exc_info=True)
        return None, None, f"❌ Error: {e}"

def get_endpoints_json() -> str:
    """Reads and returns the content of endpoints.json as a string."""
    try:
        with open(DEFAULT_CONFIG_FILE, "r") as f:
            return f.read()
    except Exception as e:
        logger.error(f"Could not read {DEFAULT_CONFIG_FILE}: {e}")
        raise gr.Error(f"Server could not read its {DEFAULT_CONFIG_FILE} configuration file.")

# ==============================================================================
#  GRADIO INTERFACE
# ==============================================================================

with gr.Blocks(theme=gr.themes.Soft(), title="KeyLock Image Creator") as demo:
    config_state = gr.State(initial_config)
    
    gr.Markdown("# 🏭 KeyLock Image Creator")
    gr.Markdown("Create secure, encrypted PNG images for various decoder services.")

    with gr.Tabs() as tabs:
        with gr.TabItem("Create Image", id=0):
            # ... (Your existing UI code is fine, no changes needed here) ...
            with gr.Row():
                with gr.Column(scale=2):
                    gr.Markdown("### 1. Select Decoder Service")
                    service_dropdown = gr.Dropdown(
                        label="Target Service",
                        choices=[s['name'] for s in initial_config],
                        value=[s['name'] for s in initial_config][0] if initial_config else None,
                        interactive=True
                    )
                    gr.Markdown("### 2. Enter Your Secret Data")
                    secret_data = gr.Textbox(
                        lines=7,
                        label="Secret Data (Key-Value Pairs)",
                        placeholder="USERNAME: user@example.com\nAPI_KEY: sk-12345..."
                    )
                with gr.Column(scale=1):
                    gr.Markdown("### 3. Generate")
                    create_button = gr.Button("Create Encrypted Image", variant="primary")
                    status_output = gr.Textbox(label="Status", interactive=False, lines=2)
                    image_output = gr.Image(label="Generated Image", type="pil")
                    download_output = gr.File(label="Download Image")

        with gr.TabItem("Manage Configuration", id=1):
            # ... (Your existing config management code is fine, no changes needed here) ...
            gr.Markdown("## Decoder Service Configuration")
            gr.Markdown("View the current configuration, add new services, and save your changes.")
            with gr.Row():
                with gr.Column(scale=2):
                    gr.Markdown("### Current Configuration")
                    config_display = gr.JSON(value=initial_config, label=f"Loaded from {DEFAULT_CONFIG_FILE}")
                with gr.Column(scale=1):
                    gr.Markdown("### Add a New Service")
                    new_service_name = gr.Textbox(label="Service Name", placeholder="e.g., My Production API")
                    new_service_link = gr.Textbox(label="Service Link", placeholder="https://huggingface.co/...")
                    new_service_key = gr.Textbox(label="Public Key (PEM Format)", lines=5, placeholder="-----BEGIN PUBLIC KEY-----...")
                    add_service_button = gr.Button("Add Service to Current Session")
            gr.Markdown("---")
            save_config_button = gr.Button("Save Current Configuration to File", variant="secondary", visible=False)
            config_status_output = gr.Textbox(label="Configuration Status", interactive=False)
            
        # FIX: Define API endpoints clearly instead of using a hidden row
        with gr.TabItem("API", id=2):
            gr.Markdown("## API Endpoints")
            gr.Markdown("Use these endpoints for programmatic access.")
            
            # API for creating an image
            gr.Interface(
                # By passing core.create_encrypted_image directly, we ensure it runs in the correct scope
                fn=core.create_encrypted_image,
                inputs=[
                    gr.Textbox(label="Secret Data", info="Key-value pairs, one per line."),
                    gr.Textbox(label="Public Key PEM", info="The full PEM-formatted public key.")
                ],
                outputs=gr.Image(type="pil", label="Encrypted Image"),
                title="Image Creation API",
                description="Creates an encrypted image from secret data and a public key. Use via the `/api/create_image/` route.",
                api_name="create_image" 
            )

            # API to get the endpoint list
            gr.Interface(
                fn=get_endpoints_json,
                inputs=[],
                outputs=gr.Textbox(label="Endpoint Configuration"),
                title="Get Endpoints API",
                description=f"Retrieves the contents of `{DEFAULT_CONFIG_FILE}`. Use via the `/api/get_endpoints/` route.",
                api_name="get_endpoints"
            )

    # --- Interactivity ---
    
    create_button.click(
        fn=generate_image,
        inputs=[service_dropdown, secret_data, config_state],
        outputs=[image_output, download_output, status_output]
    )

    add_service_button.click(
        fn=add_new_service,
        inputs=[config_state, new_service_name, new_service_link, new_service_key],
        outputs=[config_state, service_dropdown, config_status_output, new_service_name, new_service_link, new_service_key]
    ).then(
        lambda state: state,
        inputs=[config_state],
        outputs=[config_display]
    )
    
    save_config_button.click(
        fn=save_config_to_file,
        inputs=[gr.State(DEFAULT_CONFIG_FILE), config_state],
        outputs=[config_status_output]
    )

    config_state.change(
        fn=lambda state: state,
        inputs=[config_state],
        outputs=[config_display]
    )

if __name__ == "__main__":
    demo.launch()