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 @@
- - {% if meme.upload_path.endswith('mp4') %} + + {% if meme.file_name.endswith('mp4') %} {% else %} - {{ meme.title }} + {{ meme.title }} {% endif %}
diff --git a/suchwowx/templates/meme.html b/suchwowx/templates/meme.html index edae466..939f490 100644 --- a/suchwowx/templates/meme.html +++ b/suchwowx/templates/meme.html @@ -10,18 +10,18 @@ {% if meme %}
- {% if meme.upload_path.endswith('mp4') %} + {% if meme.file_name.endswith('mp4') %} {% else %} - + {% endif %}

Title: {{ meme.title }}

Description: {{ meme.description }}

-

Creator handle: {{ meme.creator_handle }}

+

Creator handle: {{ meme.creator_handle }}

Meta IPFS: {{ meme.meta_ipfs_hash }}

Meme IPFS: {{ meme.meme_ipfs_hash }}

Meme ID: {{ meme }}

diff --git a/suchwowx/templates/new.html b/suchwowx/templates/publish.html similarity index 92% rename from suchwowx/templates/new.html rename to suchwowx/templates/publish.html index e198aa7..4a66694 100644 --- a/suchwowx/templates/new.html +++ b/suchwowx/templates/publish.html @@ -10,9 +10,9 @@

Memes. Interplanetary!

- Go Back + Go Back -
+