|
| 1 | +# app.py |
| 2 | +import base64 |
| 3 | +import hashlib |
| 4 | +import threading |
| 5 | +import time |
| 6 | +from datetime import datetime, timedelta |
| 7 | +from functools import wraps |
| 8 | +from random import Random |
| 9 | + |
| 10 | +from flask import ( |
| 11 | + Flask, |
| 12 | + jsonify, |
| 13 | + make_response, |
| 14 | + redirect, |
| 15 | + render_template_string, |
| 16 | + request, |
| 17 | +) |
| 18 | + |
| 19 | +random_0 = Random(0) |
| 20 | +app = Flask(__name__) |
| 21 | + |
| 22 | +# In-memory database (replace with a real database in production) |
| 23 | +database = {} |
| 24 | + |
| 25 | +# Email storage for development |
| 26 | +email_storage = [] |
| 27 | +email_lock = threading.Lock() |
| 28 | +email_condition = threading.Condition(email_lock) |
| 29 | + |
| 30 | + |
| 31 | +class DevTime: |
| 32 | + def __init__(self): |
| 33 | + self.current_time = datetime(2000, 1, 1) |
| 34 | + |
| 35 | + def set_year(self, year): |
| 36 | + self.current_time = datetime(year, 1, 1) |
| 37 | + |
| 38 | + def advance_24hrs(self): |
| 39 | + self.current_time += timedelta(days=1) |
| 40 | + |
| 41 | + def now(self): |
| 42 | + return self.current_time |
| 43 | + |
| 44 | + |
| 45 | +dev_time = DevTime() |
| 46 | + |
| 47 | + |
| 48 | +def repeatable_random(length): |
| 49 | + # This is a simplified version, not as secure as Java's SecureRandom |
| 50 | + return "".join( |
| 51 | + random_0.choice( |
| 52 | + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" |
| 53 | + ) |
| 54 | + for _ in range(length) |
| 55 | + ) |
| 56 | + |
| 57 | + |
| 58 | +def send_email(to_email, subject, html_content): |
| 59 | + email = {"to": to_email, "subject": subject, "html_content": html_content} |
| 60 | + with email_lock: |
| 61 | + email_storage.append(email) |
| 62 | + email_condition.notify_all() |
| 63 | + |
| 64 | + |
| 65 | +def sign_email(email): |
| 66 | + terrible_security = "password" |
| 67 | + return base64.urlsafe_b64encode( |
| 68 | + hashlib.sha256(f"{email}{terrible_security}".encode()).digest() |
| 69 | + ).decode() |
| 70 | + |
| 71 | + |
| 72 | +def auth_required(f): |
| 73 | + @wraps(f) |
| 74 | + def decorated_function(*args, **kwargs): |
| 75 | + user = auth_user() |
| 76 | + if user is None: |
| 77 | + return redirect("/") |
| 78 | + return f(user, *args, **kwargs) |
| 79 | + |
| 80 | + return decorated_function |
| 81 | + |
| 82 | + |
| 83 | +def auth_user(): |
| 84 | + login_cookie = request.cookies.get("login") |
| 85 | + if not login_cookie: |
| 86 | + return None |
| 87 | + email, signature = login_cookie.split("|") |
| 88 | + if signature != sign_email(email): |
| 89 | + return None |
| 90 | + return {"email": email} |
| 91 | + |
| 92 | + |
| 93 | +@app.route("/") |
| 94 | +def index(): |
| 95 | + user = auth_user() |
| 96 | + if user: |
| 97 | + return render_template_string( |
| 98 | + """ |
| 99 | +<html><body> |
| 100 | + <h1>Welcome back {{ username }}</h1> |
| 101 | +</body></html>""", |
| 102 | + username=user["email"], |
| 103 | + ) |
| 104 | + else: |
| 105 | + return render_template_string( |
| 106 | + """ |
| 107 | +<html><body> |
| 108 | + <h1>Please login</h1> |
| 109 | + <form action="/login" method="post"> |
| 110 | + <input type="text" name="email" placeholder="email"> |
| 111 | + <input type="submit" value="login"> |
| 112 | + </form> |
| 113 | +</body></html>""" |
| 114 | + ) |
| 115 | + |
| 116 | + |
| 117 | +@app.route("/login", methods=["POST"]) |
| 118 | +def login(): |
| 119 | + email = request.form["email"] |
| 120 | + random_code = repeatable_random(7) |
| 121 | + database[random_code] = email |
| 122 | + |
| 123 | + login_link = f"http://{request.host}/login-confirm/{random_code}" |
| 124 | + send_email( |
| 125 | + email, |
| 126 | + "Login to example.com", |
| 127 | + f'Click <a href="{login_link}">here</a> to login.', |
| 128 | + ) |
| 129 | + |
| 130 | + return render_template_string( |
| 131 | + """ |
| 132 | +<html><body> |
| 133 | + <h1>Email sent!</h1> |
| 134 | + <p>Check your email for your login link.</p> |
| 135 | +</body></html>""" |
| 136 | + ) |
| 137 | + |
| 138 | + |
| 139 | +@app.route("/login-confirm/<code>") |
| 140 | +def login_confirm(code): |
| 141 | + email = database.pop(code, None) |
| 142 | + if email is None: |
| 143 | + return render_template_string( |
| 144 | + """ |
| 145 | +<html><body> |
| 146 | + <h1>Login link expired.</h1> |
| 147 | + <p>Sorry, <a href="/">try again</a>.</p> |
| 148 | +</body></html>""" |
| 149 | + ) |
| 150 | + response = make_response(redirect("/")) |
| 151 | + response.set_cookie("login", f"{email}|{sign_email(email)}") |
| 152 | + return response |
| 153 | + |
| 154 | + |
| 155 | +@app.route("/email") |
| 156 | +def email_list(): |
| 157 | + messages = email_storage |
| 158 | + html = "<h2>Messages</h2><ul>" |
| 159 | + if not messages: |
| 160 | + html += "<li>(none)</li>" |
| 161 | + else: |
| 162 | + for i, message in enumerate(messages, 1): |
| 163 | + html += f'<li><a href="/email/message/{i}">{i}: {message["to"]} {message["subject"]}</a></li>' |
| 164 | + html += "</ul>" |
| 165 | + return html |
| 166 | + |
| 167 | + |
| 168 | +@app.route("/email/message/<int:idx>") |
| 169 | +def email_message(idx): |
| 170 | + idx -= 1 |
| 171 | + if 0 <= idx < len(email_storage): |
| 172 | + return email_storage[idx]["html_content"] |
| 173 | + else: |
| 174 | + return "No such message" |
| 175 | + |
| 176 | + |
| 177 | +def wait_for_incoming_email(timeout=1): |
| 178 | + start_time = time.time() |
| 179 | + with email_lock: |
| 180 | + while len(email_storage) == 0: |
| 181 | + remaining_time = timeout - (time.time() - start_time) |
| 182 | + if remaining_time <= 0: |
| 183 | + raise TimeoutError("Email wasn't sent within the specified timeout") |
| 184 | + email_condition.wait(timeout=remaining_time) |
| 185 | + return email_storage[-1] |
| 186 | + |
| 187 | + |
| 188 | +@app.route("/dev/time", methods=["POST"]) |
| 189 | +def set_dev_time(): |
| 190 | + json = request.json |
| 191 | + assert json is not None |
| 192 | + action = json.get("action") |
| 193 | + if action == "set_year": |
| 194 | + year = json.get("year") |
| 195 | + dev_time.set_year(year) |
| 196 | + elif action == "advance_24hrs": |
| 197 | + dev_time.advance_24hrs() |
| 198 | + return jsonify({"current_time": dev_time.now().isoformat()}) |
| 199 | + |
| 200 | + |
| 201 | +if __name__ == "__main__": |
| 202 | + print("Opening selfie demo app at http://localhost:5000") |
| 203 | + app.run(debug=True) |
0 commit comments