my eye

Bootstrap

Committed 5a230f

index 0000000..afadc08
--- /dev/null

+[tool.poetry]
+name = "webint-owner"
+version = "0.0.15"
+description = "manage website ownership"
+authors = ["Angelo Gladding <angelo@ragt.ag>"]
+license = "BSD-2-Clause"
+packages = [{include="webint_owner"}]
+
+[tool.poetry.plugins."webapps"]
+owner = "webint_owner:app"
+
+[tool.poetry.dependencies]
+python = ">=3.10,<3.11"
+webint = ">=0.0"
+
+[tool.poetry.group.dev.dependencies]
+gmpg = {path="../gmpg", develop=true}
+bgq = {path="../bgq", develop=true}
+newmath = {path="../newmath", develop=true}
+sqlyte = {path="../sqlyte", develop=true}
+webagt = {path="../webagt", develop=true}
+webint = {path="../webint", develop=true}
+microformats = {path="../python-microformats", develop=true}
+
+# [[tool.poetry.source]]
+# name = "main"
+# url = "https://ragt.ag/code/pypi"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"

index 0000000..e4783ac
--- /dev/null

+"""
+Manage your website's ownership details.
+
+"""
+
+import os
+import pathlib
+import subprocess
+
+import web
+from web import tx
+
+app = web.application(
+    __name__,
+    prefix="owner",
+    model={
+        "identities": {
+            "created": "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP",
+            "card": "JSON",
+        },
+        "passphrases": {
+            "created": "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP",
+            "passphrase_salt": "BLOB",
+            "passphrase_hash": "BLOB",
+        },
+    },
+)
+
+
+@app.query
+def get_identities(db):
+    """Return identity with given `uid`."""
+    return db.select("identities")
+
+
+@app.query
+def get_identity(db, uid="/"):
+    """Return identity with given `uid`."""
+    return db.select(
+        "identities",
+        where="json_extract(identities.card, '$.uid[0]') = ?",
+        vals=[uid],
+    )[0]
+
+
+@app.query
+def update_details(db, name, nickname, note, uid="/"):
+    """Update name of identity with given `uid`."""
+    card = db.select(
+        "identities",
+        where="json_extract(identities.card, '$.uid[0]') = ?",
+        vals=[uid],
+    )[0]["card"]
+    card["name"] = [name]
+    card["nickname"] = [nickname]
+    card["note"] = [note]
+    db.update(
+        "identities",
+        card=card,
+        where="json_extract(identities.card, '$.uid[0]') = ?",
+        vals=[uid],
+    )
+
+
+@app.query
+def add_identity(db, name, uid="/"):
+    """Create an identity."""
+    db.insert("identities", card={"uid": [uid], "name": [name]})
+
+
+@app.query
+def get_passphrase(db):
+    """Return most recent passphrase."""
+    try:
+        return db.select("passphrases", order="created DESC")[0]
+    except IndexError:
+        return None
+
+
+@app.query
+def update_passphrase(db):
+    """Update the passphrase."""
+    passphrase_salt, passphrase_hash, passphrase = web.generate_passphrase()
+    db.insert(
+        "passphrases",
+        passphrase_salt=passphrase_salt,
+        passphrase_hash=passphrase_hash,
+    )
+    return passphrase
+
+
+@app.wrap
+def initialize_owner(handler, main_app):
+    """Ensure an owner exists and add their details to the transaction."""
+    try:
+        tx.host.owner = app.model.get_identity()["card"]
+    except IndexError:
+        secret = web.form(secret=None).secret
+        is_dev = os.getenv("WEBCTX") == "dev"
+        if secret or is_dev:
+            if main_app.cfg.get("SECRET") != secret and not is_dev:
+                raise web.BadRequest("bad secret")
+            app.model.add_identity("Unnamed")
+            passphrase = " ".join(app.model.update_passphrase())
+            tx.host.owner = tx.user.session = app.model.get_identity()["card"]
+            tx.user.is_owner = True
+            web.header("Content-Type", "text/html")
+            raise web.Created(app.view.claimed(tx.origin, passphrase), tx.origin)
+        raise web.NotFound("no identity initialized")
+    tx.user.is_owner = tx.user.session.get("uid", [None])[0] == "/"
+    yield
+
+
+@app.wrap
+def authorize_owner(handler, main_app):
+    """Manage access to owner-only resources."""
+    if not tx.user.is_owner and tx.request.method.lower() in getattr(
+        handler, "owner_only", []
+    ):
+        raise web.Unauthorized(app.view.unauthorized())
+    yield
+
+
+@app.control("")
+class Owner:
+    """Owner information."""
+
+    owner_only = ["get", "post"]
+
+    def get(self):
+        """Render site owner information."""
+        return app.view.index()
+
+    def post(self):
+        """Update owner information."""
+        form = web.form("name", "nickname", "note")
+        app.model.update_details(form.name, form.nickname, form.note)
+        raise web.SeeOther("/owner")
+
+
+@app.control(r"sign-in")
+class SignIn:
+    """Sign in as the owner of the site."""
+
+    def get(self):
+        """Verify a sign-in or render the sign-in form."""
+        try:
+            self.verify_passphrase()
+        except web.BadRequest:
+            if tx.user.is_owner:
+                raise web.SeeOther("/")
+            return_to = web.form(return_to="").return_to
+            return app.view.signin(return_to)
+
+    def post(self):
+        """Verify a sign-in."""
+        self.verify_passphrase()
+
+    def verify_passphrase(self):
+        """Verify passphrase, sign the owner in and return to given return page."""
+        form = web.form("passphrase", return_to="")
+        passphrase = app.model.get_passphrase()
+        if web.verify_passphrase(
+            passphrase["passphrase_salt"],
+            passphrase["passphrase_hash"],
+            form.passphrase.replace(" ", ""),
+        ):
+            tx.user.session = app.model.get_identity()["card"]
+            raise web.SeeOther(f"/{form.return_to}")
+        raise web.Unauthorized("bad passphrase")
+
+
+@app.control("sign-out")
+class SignOut:
+    """Sign out while signed in as the owner of the site."""
+
+    owner_only = ["get", "post"]
+
+    def get(self):
+        """Return the sign out form."""
+        return app.view.signout()
+
+    def post(self):
+        """Sign the owner out and return to given return page."""
+        tx.user.session = None
+        return_to = web.form(return_to="").return_to
+        raise web.SeeOther(f"/{return_to}")
+
+
+@app.control("reset")
+class Reset:
+    """Reset the passphrase. You must manually delete the current passphrase first."""
+
+    def get(self):
+        """Return the new passphrase."""
+        if not app.model.get_passphrase():
+            return "Your new password: " + " ".join(app.model.update_passphrase())
+        return "Passphrase must be manually deleted first."
+
+
+@app.control("actor", prefixed=False)
+class ActivityPubActor:
+    """."""
+
+    def get(self):
+        """."""
+        ap_pubkey = pathlib.Path("ap_public.pem")
+        ap_pvtkey = pathlib.Path("ap_private.pem")
+        if not ap_pubkey.exists():
+            subprocess.run(["openssl", "genrsa", "-out", ap_pvtkey, "2048"])
+            subprocess.run(
+                [
+                    "openssl",
+                    "rsa",
+                    "-in",
+                    ap_pvtkey,
+                    "-outform",
+                    "PEM",
+                    "-pubout",
+                    "-out",
+                    ap_pubkey,
+                ]
+            )
+        with ap_pubkey.open() as fp:
+            pubkey = fp.read().strip()
+        web.header("Content-Type", "application/ld+json")
+        return {
+            "@context": [
+                "https://www.w3.org/ns/activitystreams",
+                "https://w3id.org/security/v1",
+            ],
+            "id": f"{tx.origin}/actor",
+            "type": "Person",
+            "preferredUsername": tx.host.owner["nickname"][0],
+            "name": tx.host.owner["name"][0],
+            "summary": tx.host.owner["note"][0],
+            "inbox": f"{tx.origin}/inbox",
+            "publicKey": {
+                "id": f"{tx.origin}/actor#main-key",
+                "owner": f"{tx.origin}/actor",
+                "publicKeyPem": pubkey,
+            },
+        }
+
+
+@app.control(".well-known/webfinger", prefixed=False)
+class WebfingerProfile:
+    """."""
+
+    def get(self):
+        """."""
+        web.header("Content-Type", "application/jrd+json")
+        return {
+            "subject": f"acct:{tx.host.owner['nickname'][0]}@{tx.host.name}",
+            "aliases": [tx.origin],
+            "links": [
+                {
+                    "rel": "http://webfinger.net/rel/profile-page",
+                    "type": "text/html",
+                    "href": tx.origin,
+                },
+                {
+                    "rel": "self",
+                    "type": "application/activity+json",
+                    "href": f"{tx.origin}/actor",
+                },
+                {
+                    "rel": "http://ostatus.org/schema/1.0/subscribe",
+                    "template": f"{tx.origin}/subscriptions?uri={{uri}}",
+                },
+            ],
+        }

index 0000000..9f63652
--- /dev/null

+from web import tx
+
+__all__ = ["tx"]

index 0000000..a36d21e
--- /dev/null

+$def with ()
+<title>Claim This Domain</title>
+<h1>Claim This Domain</h1>
+<p>This domain has not yet been claimed.</p>
+<form action=/owner/claim method=post>
+<div><label>Your Full Name<br>
+<input type=text name=name></label></div>
+<button>Claim</button>
+</form>

index 0000000..3e0e72b
--- /dev/null

+$def with (uid, passphrase)
+$var title: Site has been claimed
+
+<script>
+document.addEventListener('DOMContentLoaded', () => {
+  try {
+    window.navigator.registerProtocolHandler(
+      'web+action', '$uid/actions?handler=%s', '$uid'
+    )
+  } catch(err) { // if not in https context
+    // iOS fix
+  }
+})
+</script>
+
+<p>You are now the owner. This is your passphrase. Write it down.</p>
+
+<pre id=passphrase>$passphrase</pre>

index 0000000..1a8f90a
--- /dev/null

+$def with ()
+$var title: Site Owner
+
+$ get = lambda property: tx.host.owner.get(property, [None])
+
+<form method=post>
+<div><label>Your address<br>
+<input type=text readonly value="$get('uid')[0]"></div>
+<div><label>Name<br>
+<input type=text name=name value="$get('name')[0]"></div>
+<div><label>Nickname<br>
+<input type=text name=nickname value="$get('nickname')[0]"></div>
+<div><label>Note<br>
+<input type=text name=note value="$get('note')[0]"></div>
+<button>Update</button>
+</form>

index 0000000..6cd739a
--- /dev/null

+$def with (return_to)
+$var title: Owner Sign-in
+
+<form method=post action=/sign-in>
+<input type=hidden name=return_to value="$return_to">
+<div><label>Passphrase<br>
+<input type=text name=passphrase></label></div>
+<button>Sign In</button>
+</form>

index 0000000..3185e12
--- /dev/null

+$def with ()
+$var title: Sign out
+
+<form method=post>
+<button>Sign Out</button>
+</form>

index 0000000..542db41
--- /dev/null

+$var title: Unauthorized
+
+<p>You must be the site owner. <a
+href=$tx.origin/owner/sign-in?return_to=$tx.request.uri.path>Sign in</a> if you are.</p>