feat: Add HNS.ID domains using ETH signing
All checks were successful
Build Docker / Build Docker (push) Successful in 3m47s
All checks were successful
Build Docker / Build Docker (push) Successful in 3m47s
This commit is contained in:
parent
0bc07e491e
commit
7ea097f916
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,4 +3,6 @@ __pycache__/
|
|||||||
|
|
||||||
instance/
|
instance/
|
||||||
|
|
||||||
website/avatars/
|
website/avatars/
|
||||||
|
|
||||||
|
.env
|
@ -3,4 +3,7 @@ Flask-SQLAlchemy
|
|||||||
Authlib
|
Authlib
|
||||||
requests
|
requests
|
||||||
dnspython
|
dnspython
|
||||||
requests-doh
|
requests-doh
|
||||||
|
python-dotenv
|
||||||
|
web3
|
||||||
|
eth-account
|
@ -3,7 +3,10 @@ from flask import Flask
|
|||||||
from .models import db
|
from .models import db
|
||||||
from .oauth2 import config_oauth
|
from .oauth2 import config_oauth
|
||||||
from .routes import bp
|
from .routes import bp
|
||||||
|
from datetime import timedelta
|
||||||
|
import dotenv
|
||||||
|
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
def create_app(config=None):
|
def create_app(config=None):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@ -15,18 +18,26 @@ def create_app(config=None):
|
|||||||
if 'WEBSITE_CONF' in os.environ:
|
if 'WEBSITE_CONF' in os.environ:
|
||||||
app.config.from_envvar('WEBSITE_CONF')
|
app.config.from_envvar('WEBSITE_CONF')
|
||||||
|
|
||||||
|
# Set the secret key to a key from the ENV
|
||||||
|
app.secret_key = os.getenv("FLASK_SECRET_KEY", os.urandom(24).hex())
|
||||||
|
|
||||||
|
# Set the session to be permanent
|
||||||
|
app.config["SESSION_PERMANENT"] = True
|
||||||
|
|
||||||
|
# Set it to 6 months
|
||||||
|
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=180)
|
||||||
|
|
||||||
# load app specified configuration
|
# load app specified configuration
|
||||||
if config is not None:
|
if config is not None:
|
||||||
if isinstance(config, dict):
|
if isinstance(config, dict):
|
||||||
app.config.update(config)
|
app.config.update(config)
|
||||||
elif config.endswith('.py'):
|
elif config.endswith('.py'):
|
||||||
app.config.from_pyfile(config)
|
app.config.from_pyfile(config)
|
||||||
|
|
||||||
setup_app(app)
|
setup_app(app)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def setup_app(app):
|
def setup_app(app: Flask):
|
||||||
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
# Create tables if they do not exist already
|
# Create tables if they do not exist already
|
||||||
|
@ -13,9 +13,15 @@ import dns.message
|
|||||||
import dns.query
|
import dns.query
|
||||||
import dns.rdatatype
|
import dns.rdatatype
|
||||||
from requests_doh import DNSOverHTTPSSession, add_dns_provider
|
from requests_doh import DNSOverHTTPSSession, add_dns_provider
|
||||||
|
from datetime import timedelta
|
||||||
|
from eth_account.messages import encode_defunct
|
||||||
|
from eth_account import Account
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("home", __name__)
|
bp = Blueprint("home", __name__)
|
||||||
|
openSeaAPIKey = os.getenv("OPENSEA_API_KEY")
|
||||||
|
|
||||||
|
|
||||||
if not os.path.exists("website/avatars"):
|
if not os.path.exists("website/avatars"):
|
||||||
os.makedirs("website/avatars")
|
os.makedirs("website/avatars")
|
||||||
@ -45,6 +51,8 @@ def home():
|
|||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
session["id"] = user.id
|
session["id"] = user.id
|
||||||
|
# Make sure the session is permanent
|
||||||
|
session.permanent = True
|
||||||
# if user is not just to log in, but need to head back to the auth page, then go for it
|
# if user is not just to log in, but need to head back to the auth page, then go for it
|
||||||
if next_page:
|
if next_page:
|
||||||
return redirect(next_page)
|
return redirect(next_page)
|
||||||
@ -57,8 +65,68 @@ def home():
|
|||||||
else:
|
else:
|
||||||
clients = []
|
clients = []
|
||||||
|
|
||||||
return render_template("home.html", user=user, clients=clients)
|
# Check if the user has signed in with HNS ID
|
||||||
|
hnsid=''
|
||||||
|
address=''
|
||||||
|
if "address" in session:
|
||||||
|
address = session["address"]
|
||||||
|
openseaInfo = requests.get("https://api.opensea.io/api/v2/chain/optimism/account/{address}/nfts?collection=handshake-slds",
|
||||||
|
headers={"Accept": "application/json",
|
||||||
|
"x-api-key":openSeaAPIKey})
|
||||||
|
if openseaInfo.status_code == 200:
|
||||||
|
hnsid = openseaInfo.json()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return render_template("home.html", user=user, clients=clients, address=address, hnsid=hnsid)
|
||||||
|
|
||||||
|
@bp.route("/hnsid", methods=["POST"])
|
||||||
|
def hnsid():
|
||||||
|
# Get address and signature from the request
|
||||||
|
address = request.json.get("address")
|
||||||
|
signature = request.json.get("signature")
|
||||||
|
message = request.json.get("message")
|
||||||
|
# Verify the signature
|
||||||
|
msg = encode_defunct(text=message)
|
||||||
|
signer = Account.recover_message(msg, signature=signature).lower()
|
||||||
|
if signer != address:
|
||||||
|
print("Signature verification failed")
|
||||||
|
print(signer, address)
|
||||||
|
return jsonify({"success": False})
|
||||||
|
|
||||||
|
# Save the address in the session
|
||||||
|
session["address"] = address
|
||||||
|
session.permanent = True
|
||||||
|
|
||||||
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
@bp.route("/hnsid/<domain>")
|
||||||
|
def hnsid_domain(domain):
|
||||||
|
# Get the address from the session
|
||||||
|
address = session.get("address")
|
||||||
|
if not address:
|
||||||
|
return jsonify({"error": "No address found in session"})
|
||||||
|
|
||||||
|
# Get domain info from Opensea
|
||||||
|
openseaInfo = requests.get(f"https://api.opensea.io/api/v2/chain/optimism/account/{address}/nfts?collection=handshake-slds",
|
||||||
|
headers={"Accept": "application/json",
|
||||||
|
"x-api-key":openSeaAPIKey})
|
||||||
|
if openseaInfo.status_code != 200:
|
||||||
|
return jsonify({"error": "Failed to get domain info from Opensea"})
|
||||||
|
hnsid = openseaInfo.json()
|
||||||
|
for nft in hnsid["nfts"]:
|
||||||
|
if nft["name"] == domain:
|
||||||
|
# Add domain to the session
|
||||||
|
user = User.query.filter_by(username=domain).first()
|
||||||
|
if not user:
|
||||||
|
user = User(username=domain)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
session["id"] = user.id
|
||||||
|
session.permanent = True
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
return jsonify({"success": False, "error": "Domain not found"})
|
||||||
|
|
||||||
@bp.route("/logout")
|
@bp.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
|
@ -1,80 +1,104 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>HNS Login</title>
|
<title>HNS Login</title>
|
||||||
<link rel="icon" href="favicon.png" type="image/png">
|
<link rel="icon" href="favicon.png" type="image/png">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
/* Dark theme*/
|
/* Dark theme*/
|
||||||
background-color: #222;
|
background-color: #222;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #333;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
form {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
background-color: #333;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
a.button {
|
|
||||||
display: block;
|
|
||||||
width: 200px;
|
|
||||||
margin: 20px auto;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
background-color: #333;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
button.loginbutton {
|
|
||||||
/* Put in the centre of the screen */
|
|
||||||
|
|
||||||
margin-left: 50%;
|
h1 {
|
||||||
transform: translateX(-50%);
|
margin: 0;
|
||||||
}
|
padding: 20px;
|
||||||
</style>
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button {
|
||||||
|
display: block;
|
||||||
|
width: 200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.loginbutton {
|
||||||
|
/* Put in the centre of the screen */
|
||||||
|
|
||||||
|
margin-left: 50%;
|
||||||
|
margin-top: 20px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-option {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>HNS Login</h1>
|
<h1>HNS Login</h1>
|
||||||
|
|
||||||
{% if user %}
|
{% if user %}
|
||||||
|
|
||||||
<h2>You are currently logged in as <strong>{{ user }}/</strong></h2>
|
<h2>You are currently logged in as <strong>{{ user }}/</strong></h2>
|
||||||
|
|
||||||
|
|
||||||
<a href="{{ url_for('.logout') }}" class="button">Log Out</a>
|
<a href="{{ url_for('.logout') }}" class="button">Log Out</a>
|
||||||
|
|
||||||
{% for client in clients %}
|
{% for client in clients %}
|
||||||
<pre>
|
<pre>
|
||||||
<strong>Client Info</strong>
|
<strong>Client Info</strong>
|
||||||
{%- for key in client.client_info %}
|
{%- for key in client.client_info %}
|
||||||
<strong>{{ key }}: </strong>{{ client.client_info[key] }}
|
<strong>{{ key }}: </strong>{{ client.client_info[key] }}
|
||||||
@ -84,36 +108,133 @@
|
|||||||
<strong>{{ key }}: </strong>{{ client.client_metadata[key] }}
|
<strong>{{ key }}: </strong>{{ client.client_metadata[key] }}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</pre>
|
</pre>
|
||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
<p>Want to implement OAuth?<br>
|
<p>Want to implement OAuth?<br>
|
||||||
Contact Nathan.Woodburn/ on any social media platform</p>
|
Contact Nathan.Woodburn/ on any social media platform</p>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<h2>Login with your Handshake domain</h2>
|
<h2>Login with your Handshake domain</h2>
|
||||||
<p>If you have already used Varo Auth, you can just select the domain you want to login with.</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
|
||||||
<script type="text/javascript" src="https://auth.varo.domains/v1"></script>
|
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||||
<script>var varo = new Varo();</script>
|
|
||||||
<button class="loginbutton" onclick='varo.auth().then(auth => {
|
<div class="login-option">
|
||||||
|
<script type="text/javascript" src="https://auth.varo.domains/v1"></script>
|
||||||
|
<script>var varo = new Varo();</script>
|
||||||
|
<button class="loginbutton" onclick='varo.auth().then(auth => {
|
||||||
if (auth.success) {
|
if (auth.success) {
|
||||||
// handle success by calling your api to update the users session
|
// handle success by calling your api to update the users session
|
||||||
$.post("/", JSON.stringify(auth.data), (response) => {
|
$.post("/", JSON.stringify(auth.data), (response) => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});'>Login</button>
|
});'>Login with Varo</button>
|
||||||
{% endif %}
|
</div>
|
||||||
|
|
||||||
|
<div class="login-option">
|
||||||
|
<!-- Login for HNS.ID domains -->
|
||||||
|
<script>
|
||||||
|
async function loginETH() {
|
||||||
|
if (typeof window.ethereum === 'undefined') {
|
||||||
|
alert('Please install MetaMask to use this feature');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Check if the user is already connected
|
||||||
|
const accounts = await ethereum.request({ method: 'eth_accounts' });
|
||||||
|
var address = '';
|
||||||
|
if (accounts.length > 0) {
|
||||||
|
console.log('Already connected', accounts[0]);
|
||||||
|
address = accounts[0];
|
||||||
|
} else {
|
||||||
|
console.log('Not connected yet');
|
||||||
|
// Request connection
|
||||||
|
const accounts = await ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
if (accounts.length > 0) {
|
||||||
|
address = accounts[0];
|
||||||
|
console.log('Connected', accounts[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (address != '') {
|
||||||
|
// Send the address to the server
|
||||||
|
console.log('Sending address to server', address);
|
||||||
|
// Sign a message
|
||||||
|
const message = 'I am signing my one-time nonce: ' + Math.floor(Math.random() * 1000000) + ' to log in to HNS Login as ' + address;
|
||||||
|
const signature = await ethereum.request({
|
||||||
|
method: 'personal_sign',
|
||||||
|
params: [message, address],
|
||||||
|
});
|
||||||
|
console.log('Signature', signature);
|
||||||
|
// Redirect user to choose a domain to log in with
|
||||||
|
$.ajax({
|
||||||
|
url: '/hnsid',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({ address: address, signature: signature, message: message }),
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (response) {
|
||||||
|
console.log('Response', response);
|
||||||
|
if (response.success) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking connection status', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{% if address %}
|
||||||
|
<h4 style="text-align: center;">Logged in as {{address}}</h4>
|
||||||
|
<span style="text-align: center;display: block;">Select a HNS.ID domain to log in with</span><br>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<select id="nftDropdown">
|
||||||
|
{% for nft in hnsid.nfts %}
|
||||||
|
<option value="{{nft.name}}">{{nft.name}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button onclick="HNSIDLoginSelect()">Login</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function HNSIDLoginSelect() {
|
||||||
|
var selectedNFT = document.getElementById("nftDropdown").value;
|
||||||
|
window.location.href = "/hnsid/" + selectedNFT;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div style="position: fixed; bottom: 0; width: 100%; text-align: center; background-color: #333; padding: 10px;">
|
<button class="loginbutton" onclick='javascript:loginETH();'>Login with another ETH address</button>
|
||||||
Powered by <a href="https://auth.varo.domains/implement" target="_blank">Varo Auth</a> and <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a>
|
{% else %}
|
||||||
</div>
|
<button class="loginbutton" onclick='javascript:loginETH();'>Login with HNS.ID</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div style="position: fixed; bottom: 0; width: 100%; text-align: center; background-color: #333; padding: 10px;">
|
||||||
|
Powered by <a href="https://auth.varo.domains/implement" target="_blank">Varo Auth</a> and <a
|
||||||
|
href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
Reference in New Issue
Block a user