Compare commits

...

4 Commits

Author SHA1 Message Date
2f099b7c07 feat: Add onchain TXT record login using FIRE HSD
All checks were successful
Build Docker / Build Docker (push) Successful in 34s
2025-11-20 10:39:55 +11:00
03fe2b552c fix: Catch DNS from wireformat error
All checks were successful
Build Docker / Build Docker (push) Successful in 1m39s
2025-11-20 10:15:52 +11:00
efd94281ef Revert "feat: Try to migrate db"
All checks were successful
Build Docker / Build Docker (push) Successful in 45s
This reverts commit eaf416316a.
2025-10-22 12:55:35 +11:00
e959856679 Revert "fix: Try to fix db"
This reverts commit d1f35096e5.
2025-10-22 12:55:22 +11:00
9 changed files with 1743 additions and 166 deletions

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

6
main.py Normal file
View File

@@ -0,0 +1,6 @@
def main():
print("Hello from hns-login!")
if __name__ == "__main__":
main()

View File

@@ -1,10 +0,0 @@
#!/usr/bin/env python3
from website import create_app
from website.migrations import add_missing_columns_to_oauth2_code
if __name__ == '__main__':
app = create_app()
print("Running database migration...")
add_missing_columns_to_oauth2_code(app)
print("Migration completed.")

22
pyproject.toml Normal file
View File

@@ -0,0 +1,22 @@
[project]
name = "hns-login"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"authlib>=1.6.5",
"dnspython>=2.6.1",
"eth-account>=0.13.7",
"flask>=3.1.2",
"flask-sqlalchemy>=3.1.1",
"python-dotenv>=1.2.1",
"requests>=2.32.3",
"requests-doh>=1.0.0",
"web3>=7.14.0",
]
[dependency-groups]
dev = [
"ruff>=0.14.5",
]

1619
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +0,0 @@
import os
from flask import Flask
def create_app():
app = Flask(__name__)
app.config.from_object(os.environ['APP_SETTINGS'])
with app.app_context():
# Run migrations first, before any database operations
from .migrations import add_missing_columns_to_oauth2_code
add_missing_columns_to_oauth2_code(app)
# Import models after migration but before init_db
from .models import db
db.create_all()
return app

View File

@@ -1,91 +0,0 @@
import os
import sqlite3
from sqlalchemy import inspect
from flask import current_app
import logging
def add_missing_columns_to_oauth2_code(app):
"""
Check and add missing columns to oauth2_code table
"""
print("Starting database migration check...")
with app.app_context():
from website.models import db
# Get the engine and inspector
engine = db.engine
inspector = inspect(engine)
# Check if oauth2_code table exists
if 'oauth2_code' not in inspector.get_table_names():
print("oauth2_code table doesn't exist yet, skipping migration")
return # Table doesn't exist yet
# Get existing columns
columns = [column['name'] for column in inspector.get_columns('oauth2_code')]
print(f"Existing columns in oauth2_code: {columns}")
# Define columns that should be added if missing
missing_columns = {
'acr': 'TEXT',
'amr': 'TEXT',
'code_challenge': 'TEXT',
'code_challenge_method': 'TEXT'
}
# Check which columns need to be added
columns_to_add = {col: dtype for col, dtype in missing_columns.items() if col not in columns}
if not columns_to_add:
print("No columns need to be added, schema is up to date")
return # No columns need to be added
print(f"Columns to add: {columns_to_add}")
# Connect directly to SQLite to add columns
try:
# Get database URI from app config
db_uri = current_app.config.get('SQLALCHEMY_DATABASE_URI')
print(f"Database URI: {db_uri}")
# Handle both relative and absolute paths
if db_uri.startswith('sqlite:///'):
# Relative path
if db_uri.startswith('sqlite:////'):
# Absolute path
db_path = db_uri.replace('sqlite:////', '/')
else:
# Relative path - may need to be adjusted for Docker
db_path = os.path.join(app.root_path, '..', db_uri.replace('sqlite:///', ''))
else:
# Memory or other type of database
print(f"Unsupported database type: {db_uri}")
return
print(f"Attempting to connect to database at: {db_path}")
# Ensure directory exists
db_dir = os.path.dirname(db_path)
if not os.path.exists(db_dir):
print(f"Database directory doesn't exist: {db_dir}")
# Connect to database
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
for column, dtype in columns_to_add.items():
try:
sql = f'ALTER TABLE oauth2_code ADD COLUMN {column} {dtype};'
print(f"Executing SQL: {sql}")
cursor.execute(sql)
print(f"Successfully added column '{column}' to oauth2_code table")
except sqlite3.OperationalError as e:
print(f"Error adding column '{column}': {str(e)}")
conn.commit()
conn.close()
print("Migration completed successfully")
except Exception as e:
print(f"Error during migration: {str(e)}")
import traceback
traceback.print_exc()

View File

@@ -1,7 +1,7 @@
import time
import datetime as dt
from .varo_auth import flask_login as varo_auth_flask_login
from flask import Blueprint, request, session, url_for, make_response
from flask import Blueprint, request, session, url_for
from flask import render_template, redirect, jsonify, send_from_directory
from werkzeug.security import gen_salt
from authlib.integrations.flask_oauth2 import current_token
@@ -18,7 +18,6 @@ from datetime import timedelta
from eth_account.messages import encode_defunct
from eth_account import Account
import json
import urllib.parse
@@ -57,14 +56,15 @@ def split_by_crlf(s):
return [v for v in s.splitlines() if v]
def get_idns_records(domain:str) -> list:
idns_records = []
try:
query = dns.message.make_query(domain, dns.rdatatype.TXT)
dns_request = query.to_wire()
# Send the DNS query over HTTPS
response = requests.post('https://hnsdoh.com/dns-query', data=dns_request, headers={'Content-Type': 'application/dns-message'})
response = requests.post('https://au.hnsdoh.com/dns-query', data=dns_request, headers={'Content-Type': 'application/dns-message'})
# Parse the DNS response
dns_response = dns.message.from_wire(response.content)
# Loop over TXT records and look for profile
idns_records = []
for record in dns_response.answer:
if record.rdtype == dns.rdatatype.TXT:
for txt in record:
@@ -75,6 +75,30 @@ def get_idns_records(domain:str) -> list:
idns = idns.split(" ")
for r in idns:
idns_records.append(r)
except Exception as e:
print(f"Error fetching DNS records: {e}")
# Get onchain records
try:
onchain_response = requests.get(f"https://hsd.hns.au/api/v1/nameresource/{domain}")
if onchain_response.status_code == 200:
onchain_data = onchain_response.json()
if "records" in onchain_data:
for record in onchain_data["records"]:
if record["type"] == "TXT":
txt_values = record["txt"]
for txt_value in txt_values:
txt_value = txt_value.strip('"')
if txt_value.startswith("IDNS1"):
print(txt_value)
idns = txt_value.removeprefix("IDNS1 ")
idns = idns.split(" ")
for r in idns:
idns_records.append(r)
except Exception as e:
print(f"Error fetching onchain records: {e}")
return idns_records
def get_user_info(user:User) -> dict:
@@ -145,7 +169,7 @@ def home():
if request.method == "POST":
auth = varo_auth_flask_login(request)
if auth == False:
if not auth:
return redirect("/?error=login_failed")
print(auth)
user = User.query.filter_by(username=auth).first()
@@ -267,22 +291,20 @@ def hnsid_domain(domain):
@bp.route("/txt", methods=["POST"])
def txtLogin():
idns_records = []
try:
# Get domain from form
domain = request.form.get("domain").lower().strip().replace("/", "").removesuffix(".")
# Get uuid
uuid = session["uuid"]
query = dns.message.make_query(domain, dns.rdatatype.TXT)
dns_request = query.to_wire()
# Send the DNS query over HTTPS
response = requests.post('https://hnsdoh.com/dns-query', data=dns_request, headers={'Content-Type': 'application/dns-message'})
response = requests.post('https://au.hnsdoh.com/dns-query', data=dns_request, headers={'Content-Type': 'application/dns-message'})
# Parse the DNS response
dns_response = dns.message.from_wire(response.content)
# Loop over TXT records and look for profile avatar
idns_records = []
# Loop over TXT records and look for profile
for record in dns_response.answer:
if record.rdtype == dns.rdatatype.TXT:
for txt in record:
@@ -293,6 +315,31 @@ def txtLogin():
idns = idns.split(" ")
for r in idns:
idns_records.append(r)
except Exception as e:
print(f"Error fetching DNS records: {e}")
return render_template("error.html",error="The domain wasn't able to be authenticated.",
message="<br>Double check the TXT record and try again.",
custom="<button onclick='window.location.reload();'>Try again</button>"), 200
try:
# Get onchain records
onchain_response = requests.get(f"https://hsd.hns.au/api/v1/nameresource/{domain}")
if onchain_response.status_code == 200:
onchain_data = onchain_response.json()
if "records" in onchain_data:
for record in onchain_data["records"]:
if record["type"] == "TXT":
txt_values = record["txt"]
for txt_value in txt_values:
txt_value = txt_value.strip('"')
if txt_value.startswith("IDNS1"):
print(txt_value)
idns = txt_value.removeprefix("IDNS1 ")
idns = idns.split(" ")
for r in idns:
idns_records.append(r)
except Exception as e:
print(f"Error fetching onchain records: {e}")
for record in idns_records:
print(record)
@@ -623,7 +670,7 @@ def avatar(username):
dns_request = query.to_wire()
# Send the DNS query over HTTPS
response = requests.post('https://hnsdoh.com/dns-query', data=dns_request, headers={'Content-Type': 'application/dns-message'})
response = requests.post('https://au.hnsdoh.com/dns-query', data=dns_request, headers={'Content-Type': 'application/dns-message'})
# Parse the DNS response
dns_response = dns.message.from_wire(response.content)
@@ -640,7 +687,7 @@ def avatar(username):
if avatar_url != "":
# Download the avatar using DNS-over-HTTPS
add_dns_provider("hns", "https://hnsdoh.com/dns-query")
add_dns_provider("hns", "https://au.hnsdoh.com/dns-query")
session = DNSOverHTTPSSession(provider="hns")
response = session.get(avatar_url)
with open(f"website/avatars/{username}.png", "wb") as f:

View File

@@ -12,7 +12,7 @@ def flask_login(request):
def login(request):
r = requests.get(f'https://auth.shakestation.io/verify/{request}')
r = r.json()
if r['success'] == False:
if not r['success']:
return False
if 'data' in r: