From 2a9b1995ce6af8ecced4903c380f9ece99db9d8f Mon Sep 17 00:00:00 2001
From: lza_menace
Date: Mon, 27 Dec 2021 14:58:30 -0800
Subject: [PATCH] wire up metamask authentication
---
requirements.txt | 2 +
suchwowx/factory.py | 19 +++-
suchwowx/helpers.py | 13 +++
suchwowx/models.py | 52 +++++++++-
suchwowx/routes/__init__.py | 0
suchwowx/routes/api.py | 95 +++++++++++++++++++
suchwowx/{routes.py => routes/meme.py} | 37 +++-----
suchwowx/routes/meta.py | 24 +++++
suchwowx/templates/includes/navbar.html | 9 +-
suchwowx/templates/includes/scripts.html | 91 +++++++++++++++++-
suchwowx/templates/index.html | 11 ++-
suchwowx/templates/meme.html | 8 +-
suchwowx/templates/{new.html => publish.html} | 4 +-
13 files changed, 323 insertions(+), 42 deletions(-)
create mode 100644 suchwowx/helpers.py
create mode 100644 suchwowx/routes/__init__.py
create mode 100644 suchwowx/routes/api.py
rename suchwowx/{routes.py => routes/meme.py} (79%)
create mode 100644 suchwowx/routes/meta.py
rename suchwowx/templates/{new.html => publish.html} (92%)
diff --git a/requirements.txt b/requirements.txt
index 47893ab..e40f6de 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@ Flask
Flask-WTF
Flask-SQLAlchemy
Flask-Mobility
+Flask-Login
peewee
gunicorn
huey
@@ -13,3 +14,4 @@ ipfs-api
python-dotenv
requests
web3
+flake8
diff --git a/suchwowx/factory.py b/suchwowx/factory.py
index 19a399f..fb10eac 100644
--- a/suchwowx/factory.py
+++ b/suchwowx/factory.py
@@ -1,13 +1,16 @@
from logging.config import dictConfig
from flask import Flask
+from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_mobility import Mobility
+from web3 import Web3
from suchwowx import config
db = SQLAlchemy()
+w3 = Web3(Web3.HTTPProvider('http://127.0.0.1:9650')) # todo
def setup_db(app: Flask, db:SQLAlchemy=db):
@@ -28,10 +31,22 @@ def create_app():
app.config.from_envvar('FLASK_SECRETS')
setup_db(app)
Mobility(app)
+ login_manager = LoginManager(app)
+ login_manager.login_view = 'meme.index'
+ login_manager.logout_view = 'meta.disconnect'
+
+ @login_manager.user_loader
+ def load_user(user_id):
+ from suchwowx.models import User
+ user = User.query.get(user_id)
+ return user
with app.app_context():
- from suchwowx import filters, routes, cli
+ from suchwowx import filters, cli
+ from suchwowx.routes import api, meme, meta
app.register_blueprint(filters.bp)
- app.register_blueprint(routes.bp)
app.register_blueprint(cli.bp)
+ app.register_blueprint(api.bp)
+ app.register_blueprint(meme.bp)
+ app.register_blueprint(meta.bp)
return app
diff --git a/suchwowx/helpers.py b/suchwowx/helpers.py
new file mode 100644
index 0000000..5c1c99e
--- /dev/null
+++ b/suchwowx/helpers.py
@@ -0,0 +1,13 @@
+from eth_account.messages import encode_defunct
+
+from suchwowx.factory import w3
+
+
+def verify_signature(message, signature, public_address):
+ msg = encode_defunct(text=message)
+ recovered = w3.eth.account.recover_message(msg, signature=signature)
+ print(f'found recovered: {recovered}')
+ if recovered.lower() == public_address.lower():
+ return True
+ else:
+ return False
diff --git a/suchwowx/models.py b/suchwowx/models.py
index e312557..666f378 100644
--- a/suchwowx/models.py
+++ b/suchwowx/models.py
@@ -1,6 +1,8 @@
from uuid import uuid4
from datetime import datetime
+from flask_login import login_user
+
from suchwowx.factory import db
from suchwowx import config
@@ -8,13 +10,61 @@ from suchwowx import config
def rand_id():
return uuid4().hex
+class User(db.Model):
+ __tablename__ = 'users'
+
+ id = db.Column(db.Integer, primary_key=True)
+ register_date = db.Column(db.DateTime, default=datetime.utcnow())
+ last_login_date = db.Column(db.DateTime, nullable=True)
+ verified = db.Column(db.Boolean, default=False)
+ public_address = db.Column(db.String(180))
+ nonce = db.Column(db.String(180), default=rand_id())
+ nonce_date = db.Column(db.DateTime, default=datetime.utcnow())
+ handle = db.Column(db.String(40), unique=True, nullable=True)
+ bio = db.Column(db.String(600), nullable=True)
+ profile_image = db.Column(db.String(300), nullable=True)
+ website_url = db.Column(db.String(120), nullable=True)
+
+ @property
+ def is_authenticated(self):
+ return True
+
+ @property
+ def is_active(self):
+ return True
+
+ @property
+ def is_anonymous(self):
+ return False
+
+ @property
+ def is_admin(self):
+ return self.admin
+
+ def get_id(self):
+ return self.id
+
+ def generate_nonce(self):
+ return rand_id()
+
+ def change_nonce(self):
+ self.nonce = rand_id()
+ self.nonce_date = datetime.utcnow()
+ db.session.commit()
+
+ def login(self):
+ self.change_nonce()
+ self.last_login_date = datetime.utcnow()
+ login_user(self)
+ db.session.commit()
+
class Meme(db.Model):
__tablename__ = 'memes'
id = db.Column(db.String(80), default=rand_id, primary_key=True)
create_date = db.Column(db.DateTime, default=datetime.utcnow())
- upload_path = db.Column(db.String(200), unique=True)
+ file_name = db.Column(db.String(200), unique=True)
meta_ipfs_hash = db.Column(db.String(100), unique=True)
meme_ipfs_hash = db.Column(db.String(100), unique=True)
title = db.Column(db.String(50))
diff --git a/suchwowx/routes/__init__.py b/suchwowx/routes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/suchwowx/routes/api.py b/suchwowx/routes/api.py
new file mode 100644
index 0000000..d651a9d
--- /dev/null
+++ b/suchwowx/routes/api.py
@@ -0,0 +1,95 @@
+from secrets import token_urlsafe
+
+from flask import Blueprint, request, jsonify, url_for
+from flask_login import current_user
+
+from suchwowx.factory import db
+from suchwowx.helpers import verify_signature
+from suchwowx.models import User
+
+
+bp = Blueprint('api', 'api', url_prefix='/api/v1')
+
+@bp.route('/user_exists')
+def user_exists():
+ """
+ Check to see if a given user exists (handle or wallet address).
+ This logic will help the login/connect MetaMask flow.
+ """
+ if 'public_address' in request.args:
+ query_str = 'public_address'
+ query_field = User.public_address
+ elif 'handle' in request.args:
+ query_str = 'handle'
+ query_field = User.handle
+ else:
+ return jsonify({'success': False})
+
+ u = User.query.filter(
+ query_field == request.args[query_str].lower()
+ ).first()
+ if u:
+ nonce = u.nonce
+ else:
+ nonce = User().generate_nonce()
+ return jsonify({
+ 'user_exists': u is not None,
+ 'nonce': nonce,
+ 'query': query_str,
+ 'success': True
+ })
+
+@bp.route('/authenticate/metamask', methods=['POST'])
+def authenticate_metamask():
+ """
+ This is the login/authenticate route for this dApp.
+ Users POST a `signedData` blob, a message signed by the user with MetaMask
+ (`personal_sign` method).
+
+ This route will verify the signed data against the user's public ETH
+ address. If no user exists, they get an entry in the database with a
+ default handle assigned. If user does exist, they get logged in.
+ """
+ data = request.get_json()
+ if current_user.is_authenticated:
+ return jsonify({
+ 'success': False,
+ 'message': 'Already registered and authenticated.'
+ })
+
+ _u = User.query.filter_by(
+ public_address=data['public_address'].lower()
+ ).first()
+
+ if _u:
+ if data['message'].endswith(_u.nonce):
+ if verify_signature(data['message'], data['signed_data'], data['public_address']):
+ _u.login()
+ return jsonify({
+ 'success': True,
+ 'message': 'Logged in'
+ })
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': 'Invalid signature'
+ })
+ else:
+ return jsonify({
+ 'success': False,
+ 'message': 'Invalid nonce in signed message'
+ })
+ else:
+ rand_str = token_urlsafe(6)
+ user = User(
+ public_address=data['public_address'].lower()
+ )
+ db.session.add(user)
+ db.session.commit()
+ user.handle = f'anon{user.id}-{rand_str}'
+ db.session.commit()
+ user.login()
+ return jsonify({
+ 'success': True,
+ 'message': 'Registered'
+ })
diff --git a/suchwowx/routes.py b/suchwowx/routes/meme.py
similarity index 79%
rename from suchwowx/routes.py
rename to suchwowx/routes/meme.py
index 4a6ebda..2b0e833 100644
--- a/suchwowx/routes.py
+++ b/suchwowx/routes/meme.py
@@ -4,16 +4,17 @@ from json import loads, dumps
import ipfsApi
from flask import Blueprint, render_template, request, current_app
-from flask import send_from_directory, redirect, flash, url_for
+from flask import send_from_directory, redirect, flash, url_for, jsonify
+from flask_login import logout_user, current_user, login_user
from requests.exceptions import HTTPError
from web3 import Web3
-from suchwowx.models import Meme
+from suchwowx.models import Meme, User
from suchwowx.factory import db
from suchwowx import config
-bp = Blueprint('meta', 'meta')
+bp = Blueprint('meme', 'meme')
@bp.route('/')
def index():
@@ -28,12 +29,11 @@ def index():
# total_supply = contract.functions.totalSupply().call()
return render_template('index.html', memes=memes, contract=contract)
-@bp.route('/about')
-def about():
- return render_template('about.html')
-
-@bp.route('/new', methods=['GET', 'POST'])
-def new():
+@bp.route('/publish', methods=['GET', 'POST'])
+def publish():
+ if not current_user.is_authenticated:
+ flash('You need to connect your wallet first.', 'warning')
+ return redirect(url_for('meme.index'))
meme = None
form_err = False
try:
@@ -45,7 +45,7 @@ def new():
flash(msg, 'error')
if "file" in request.files:
return ''
- return redirect(url_for('meta.index'))
+ return redirect(url_for('meme.index'))
if "file" in request.files:
if form_err:
return ''
@@ -65,9 +65,7 @@ def new():
print(artwork_hashes)
artwork_hash = artwork_hashes[0]['Hash']
print(artwork_hash)
- # client.pin_add(artwork_hash)
print(f'[+] Uploaded artwork to IPFS: {artwork_hash}')
- # Create meta json
meta = {
'name': title,
'description': description,
@@ -78,10 +76,9 @@ def new():
}
}
meta_hash = client.add_json(meta)
- # client.pin_add(meta_hash)
print(f'[+] Uploaded metadata to IPFS: {meta_hash}')
meme = Meme(
- upload_path=filename,
+ file_name=filename,
meta_ipfs_hash=meta_hash,
meme_ipfs_hash=artwork_hash,
title=title,
@@ -96,16 +93,10 @@ def new():
except Exception as e:
print(e)
return render_template(
- 'new.html',
+ 'publish.html',
meme=meme
)
-@bp.route('/uploads/')
-def uploaded_file(filename):
- """
- Retrieve an uploaded file from uploads directory.
- """
- return send_from_directory(f'{config.DATA_FOLDER}/uploads', filename)
@bp.route('/meme/')
def meme(meme_id):
@@ -113,7 +104,3 @@ def meme(meme_id):
if not meme:
return redirect('/')
return render_template('meme.html', meme=meme)
-
-@bp.route('/creator/')
-def creator(handle):
- return render_template('includes/creator.html')
diff --git a/suchwowx/routes/meta.py b/suchwowx/routes/meta.py
new file mode 100644
index 0000000..e98aa92
--- /dev/null
+++ b/suchwowx/routes/meta.py
@@ -0,0 +1,24 @@
+from flask import Blueprint, render_template, send_from_directory
+from flask import redirect, url_for
+from flask_login import logout_user
+
+from suchwowx import config
+
+
+bp = Blueprint('meta', 'meta')
+
+@bp.route('/uploads/')
+def uploaded_file(filename):
+ """
+ Retrieve an uploaded file from uploads directory.
+ """
+ return send_from_directory(f'{config.DATA_FOLDER}/uploads', filename)
+
+@bp.route('/about')
+def about():
+ return render_template('about.html')
+
+@bp.route('/disconnect')
+def disconnect():
+ logout_user()
+ return redirect(url_for('meme.index'))
diff --git a/suchwowx/templates/includes/navbar.html b/suchwowx/templates/includes/navbar.html
index e5c213d..e2e33e7 100644
--- a/suchwowx/templates/includes/navbar.html
+++ b/suchwowx/templates/includes/navbar.html
@@ -7,8 +7,13 @@
{% if request.path == '/' %}
About
- New Meme
+ {% if current_user.is_authenticated %}
+ Disconnect
+ New Meme
{% else %}
- Go Home
+ Connect
+ {% endif %}
+ {% else %}
+ Go Home
{% endif %}
diff --git a/suchwowx/templates/includes/scripts.html b/suchwowx/templates/includes/scripts.html
index 585b133..5741a57 100644
--- a/suchwowx/templates/includes/scripts.html
+++ b/suchwowx/templates/includes/scripts.html
@@ -1,5 +1,5 @@
-
+
{% with messages = get_flashed_messages(with_categories=true) %}
@@ -18,3 +18,92 @@
{% endif %}
{% endwith %}
+
+{% if not current_user.is_authenticated %}
+
+
+
+{% endif %}
diff --git a/suchwowx/templates/index.html b/suchwowx/templates/index.html
index ce222c2..1e47581 100644
--- a/suchwowx/templates/index.html
+++ b/suchwowx/templates/index.html
@@ -7,13 +7,14 @@
{% include 'includes/navbar.html' %}
{% if get_flashed_messages(with_categories=true) %}
+
-
You can run your own local IPFS instance and maintain metadata and artwork,
try installing the software and running the following:
$ ipfs daemon
+
{% endif %}
@@ -25,14 +26,14 @@