File size: 6,002 Bytes
eaeb03e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# app.py  (Gate Space)
from flask import Flask, request, redirect, make_response
import html, os, time, requests, json
from itsdangerous import URLSafeSerializer

# ===== CONFIG (set as Space Secrets or edit here) =====
MODEL_API_URL  = os.getenv("MODEL_API_URL",  "")   # e.g. https://<you>-JARVIS-ModelAPI.hf.space/verify
AP_WEBHOOK_URL = os.getenv("AP_WEBHOOK_URL", "")   # optional Activepieces Catch Hook
CHATBOT_URL    = os.getenv("CHATBOT_URL",    "https://<you>-JARVIS-Chat.hf.space")
SECRET_KEY     = os.getenv("SECRET_KEY",     "change-me")
TOKEN_TTL      = int(os.getenv("TOKEN_TTL",  "3600"))  # seconds
# ===== UI TEXT =====
TITLE = "Face Verify Gate"
CAPTION = "Verify your identity to continue"
BACKGROUND_GIF = "https://i.pinimg.com/originals/d9/09/57/d90957d7462b87ba8171fce62d2bf816.gif"

app = Flask(__name__)
signer = URLSafeSerializer(SECRET_KEY, salt="jarvis-facegate")

def make_token(name: str) -> str:
    payload = {"name": name, "ts": int(time.time())}
    return signer.dumps(payload)

def page(status_msg=""):
    status_msg = html.escape(status_msg or "")
    html_page = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{TITLE}</title>
<style>
  :root {{
    --glass: rgba(10,16,24,0.55);
    --border: rgba(62,231,255,0.35);
    --accent: #3ee7ff;
    --accent2: #7bf5c8;
    --text: #e8f3ff;
    --muted: #a9c2d0;
  }}
  * {{ box-sizing: border-box; }}
  html, body {{
    height: 100%; margin: 0;
    font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
    color: var(--text);
  }}
  body {{
    background: #000 center/cover no-repeat fixed;
    background-image: url('{BACKGROUND_GIF}');
  }}
  .shade {{
    position: fixed; inset: 0;
    background: radial-gradient(60% 60% at 50% 40%, rgba(0,0,32,0.10) 0%, rgba(0,0,0,0.65) 100%);
  }}
  .card {{
    position: relative;
    max-width: 520px;
    margin: min(10vh, 8rem) auto 0;
    padding: 1.75rem 1.75rem 1.25rem;
    background: linear-gradient(180deg, rgba(10,16,24,0.72), var(--glass));
    backdrop-filter: blur(10px) saturate(130%);
    border: 1px solid var(--border);
    border-radius: 16px;
    box-shadow: 0 20px 60px rgba(0,0,0,0.6), inset 0 0 40px rgba(62,231,255,0.12);
  }}
  h1 {{ margin: 0 0 .25rem 0; font-weight: 800; letter-spacing: 0.5px; }}
  .caption {{ margin: 0 0 1.0rem 0; color: var(--muted); }}
  label {{ display:block; margin:.6rem 0 .35rem; color: var(--muted); font-weight:600; }}
  input[type="text"], input[type="file"] {{
    width: 100%; padding: .7rem .85rem;
    border-radius: 10px; border: 1px solid rgba(255,255,255,0.18);
    background: rgba(255,255,255,0.08); color: var(--text); outline: none;
  }}
  input[type="text"]:focus {{
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(62,231,255,0.25);
  }}
  button {{
    margin-top: 1rem; padding: .8rem 1rem; width: 100%;
    border-radius: 10px; border: none;
    background: linear-gradient(90deg, var(--accent), var(--accent2));
    color: #0a0f18; font-weight: 800; cursor: pointer;
  }}
  #status {{ min-height: 1.25rem; margin-top: .6rem; color: #ffd166; }}
  #status.err {{ color: #ff7b7b; }}
  #status.ok  {{ color: #7bf5c8; }}
  .stub {{ margin-top: .6rem; color: var(--muted); font-size:.9rem; }}
  @media (max-width: 520px) {{ .card {{ margin: 8vh 1rem 0; }} }}
</style>
</head>
<body>
<div class="shade"></div>
<main class="card">
  <h1>{TITLE}</h1>
  <p class="caption">{CAPTION}</p>
  <form method="POST" action="/verify" enctype="multipart/form-data" onsubmit="onSubmit()">
    <label for="name">Name</label>
    <input id="name" name="name" type="text" placeholder="Enter your name" required />
    <label for="photo">Photo</label>
    <input id="photo" name="photo" type="file" accept="image/*" required />
    <button type="submit">Verify &amp; Enter</button>
    <p id="status" aria-live="polite">{status_msg}</p>
  </form>
  <p class="stub">Jarvis mode — verification runs server-side.</p>
</main>
<script>
  function onSubmit() {{
    const s = document.getElementById('status');
    s.textContent = 'Verifying…';
    s.className = '';
  }}
</script>
</body>
</html>"""
    resp = make_response(html_page)
    resp.headers["Content-Type"] = "text/html; charset=utf-8"
    return resp

@app.get("/")
def index():
    return page("")

def call_model_api(name, file_storage):
    # Call Activepieces (preferred) or Model API directly
    url = AP_WEBHOOK_URL.strip() or MODEL_API_URL.strip()
    if not url:
        raise RuntimeError("Neither AP_WEBHOOK_URL nor MODEL_API_URL is set.")
    files = {"image" if "verify" in url else "photo": (file_storage.filename, file_storage.stream, file_storage.mimetype or "application/octet-stream")}
    data  = {"name": name}
    r = requests.post(url, data=data, files=files, timeout=45)
    # Activepieces typically returns {status, redirect_url?}; Model API returns {status, score,...}
    try:
        return r.json()
    except Exception:
        return {"status": "error", "message": f"Non-JSON from backend ({r.status_code})"}

@app.post("/verify")
def verify():
    name = (request.form.get("name") or "").strip()
    file = request.files.get("photo")
    if not name or not file or not file.filename.strip():
        return page("Please enter your name and select a photo."), 400

    try:
        result = call_model_api(name, file)
    except Exception as e:
        return page(f"System error contacting verifier: {e}"), 502

    status = str(result.get("status","")).lower()
    if status == "accepted":
        token = make_token(name)
        # Prefer redirect_url from Activepieces; else use CHATBOT_URL
        cb = result.get("redirect_url") or CHATBOT_URL
        return redirect(f"{cb}?token={token}", code=302)

    msg = result.get("message") or "Access denied by FaceGate."
    return page(msg), 401

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=True)