wire up metamask authentication

main
lza_menace 3 years ago
parent 73800d2a69
commit 2a9b1995ce

@ -4,6 +4,7 @@ Flask
Flask-WTF Flask-WTF
Flask-SQLAlchemy Flask-SQLAlchemy
Flask-Mobility Flask-Mobility
Flask-Login
peewee peewee
gunicorn gunicorn
huey huey
@ -13,3 +14,4 @@ ipfs-api
python-dotenv python-dotenv
requests requests
web3 web3
flake8

@ -1,13 +1,16 @@
from logging.config import dictConfig from logging.config import dictConfig
from flask import Flask from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_mobility import Mobility from flask_mobility import Mobility
from web3 import Web3
from suchwowx import config from suchwowx import config
db = SQLAlchemy() db = SQLAlchemy()
w3 = Web3(Web3.HTTPProvider('http://127.0.0.1:9650')) # todo
def setup_db(app: Flask, db:SQLAlchemy=db): def setup_db(app: Flask, db:SQLAlchemy=db):
@ -28,10 +31,22 @@ def create_app():
app.config.from_envvar('FLASK_SECRETS') app.config.from_envvar('FLASK_SECRETS')
setup_db(app) setup_db(app)
Mobility(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(): 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(filters.bp)
app.register_blueprint(routes.bp)
app.register_blueprint(cli.bp) app.register_blueprint(cli.bp)
app.register_blueprint(api.bp)
app.register_blueprint(meme.bp)
app.register_blueprint(meta.bp)
return app return app

@ -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

@ -1,6 +1,8 @@
from uuid import uuid4 from uuid import uuid4
from datetime import datetime from datetime import datetime
from flask_login import login_user
from suchwowx.factory import db from suchwowx.factory import db
from suchwowx import config from suchwowx import config
@ -8,13 +10,61 @@ from suchwowx import config
def rand_id(): def rand_id():
return uuid4().hex 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): class Meme(db.Model):
__tablename__ = 'memes' __tablename__ = 'memes'
id = db.Column(db.String(80), default=rand_id, primary_key=True) id = db.Column(db.String(80), default=rand_id, primary_key=True)
create_date = db.Column(db.DateTime, default=datetime.utcnow()) 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) meta_ipfs_hash = db.Column(db.String(100), unique=True)
meme_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)) title = db.Column(db.String(50))

@ -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'
})

@ -4,16 +4,17 @@ from json import loads, dumps
import ipfsApi import ipfsApi
from flask import Blueprint, render_template, request, current_app 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 requests.exceptions import HTTPError
from web3 import Web3 from web3 import Web3
from suchwowx.models import Meme from suchwowx.models import Meme, User
from suchwowx.factory import db from suchwowx.factory import db
from suchwowx import config from suchwowx import config
bp = Blueprint('meta', 'meta') bp = Blueprint('meme', 'meme')
@bp.route('/') @bp.route('/')
def index(): def index():
@ -28,12 +29,11 @@ def index():
# total_supply = contract.functions.totalSupply().call() # total_supply = contract.functions.totalSupply().call()
return render_template('index.html', memes=memes, contract=contract) return render_template('index.html', memes=memes, contract=contract)
@bp.route('/about') @bp.route('/publish', methods=['GET', 'POST'])
def about(): def publish():
return render_template('about.html') if not current_user.is_authenticated:
flash('You need to connect your wallet first.', 'warning')
@bp.route('/new', methods=['GET', 'POST']) return redirect(url_for('meme.index'))
def new():
meme = None meme = None
form_err = False form_err = False
try: try:
@ -45,7 +45,7 @@ def new():
flash(msg, 'error') flash(msg, 'error')
if "file" in request.files: if "file" in request.files:
return '<script>window.history.back()</script>' return '<script>window.history.back()</script>'
return redirect(url_for('meta.index')) return redirect(url_for('meme.index'))
if "file" in request.files: if "file" in request.files:
if form_err: if form_err:
return '<script>window.history.back()</script>' return '<script>window.history.back()</script>'
@ -65,9 +65,7 @@ def new():
print(artwork_hashes) print(artwork_hashes)
artwork_hash = artwork_hashes[0]['Hash'] artwork_hash = artwork_hashes[0]['Hash']
print(artwork_hash) print(artwork_hash)
# client.pin_add(artwork_hash)
print(f'[+] Uploaded artwork to IPFS: {artwork_hash}') print(f'[+] Uploaded artwork to IPFS: {artwork_hash}')
# Create meta json
meta = { meta = {
'name': title, 'name': title,
'description': description, 'description': description,
@ -78,10 +76,9 @@ def new():
} }
} }
meta_hash = client.add_json(meta) meta_hash = client.add_json(meta)
# client.pin_add(meta_hash)
print(f'[+] Uploaded metadata to IPFS: {meta_hash}') print(f'[+] Uploaded metadata to IPFS: {meta_hash}')
meme = Meme( meme = Meme(
upload_path=filename, file_name=filename,
meta_ipfs_hash=meta_hash, meta_ipfs_hash=meta_hash,
meme_ipfs_hash=artwork_hash, meme_ipfs_hash=artwork_hash,
title=title, title=title,
@ -96,16 +93,10 @@ def new():
except Exception as e: except Exception as e:
print(e) print(e)
return render_template( return render_template(
'new.html', 'publish.html',
meme=meme meme=meme
) )
@bp.route('/uploads/<path:filename>')
def uploaded_file(filename):
"""
Retrieve an uploaded file from uploads directory.
"""
return send_from_directory(f'{config.DATA_FOLDER}/uploads', filename)
@bp.route('/meme/<meme_id>') @bp.route('/meme/<meme_id>')
def meme(meme_id): def meme(meme_id):
@ -113,7 +104,3 @@ def meme(meme_id):
if not meme: if not meme:
return redirect('/') return redirect('/')
return render_template('meme.html', meme=meme) return render_template('meme.html', meme=meme)
@bp.route('/creator/<handle>')
def creator(handle):
return render_template('includes/creator.html')

@ -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/<path:filename>')
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'))

@ -7,8 +7,13 @@
</p> </p>
{% if request.path == '/' %} {% if request.path == '/' %}
<a class="button is-secondary" href="{{ url_for('meta.about') }}" up-target=".container">About</a> <a class="button is-secondary" href="{{ url_for('meta.about') }}" up-target=".container">About</a>
<a class="button is-primary" href="{{ url_for('meta.new') }}">New Meme</a> {% if current_user.is_authenticated %}
<a class="button is-danger" href="{{ url_for('meta.disconnect') }}">Disconnect</a>
<a class="button is-primary" href="{{ url_for('meme.publish') }}">New Meme</a>
{% else %} {% else %}
<a class="button" href="{{ url_for('meta.index') }}" up-preload up-follow=".container">Go Home</a> <a class="button is-primary" href="#" id="metamaskConnect">Connect</a>
{% endif %}
{% else %}
<a class="button" href="{{ url_for('meme.index') }}" up-preload up-follow=".container">Go Home</a>
{% endif %} {% endif %}
</div> </div>

@ -1,5 +1,5 @@
<script src="/static/js/vendor/noty-3.2.0.js"></script> <script src="/static/js/vendor/noty-3.2.0.js"></script>
<!-- <script src="/static/js/vendor/web3-1.3.6.min.js"></script> -->
<!-- <script src="/static/js/main.js"></script> --> <!-- <script src="/static/js/main.js"></script> -->
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
@ -18,3 +18,92 @@
</script> </script>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% if not current_user.is_authenticated %}
<script src="/static/js/vendor/web3-1.3.6.min.js"></script>
<script src="/static/js/vendor/metamask-onboarding-1.0.1.bundle.js"></script>
<script>
window.addEventListener('DOMContentLoaded', () => {
const onboarding = new MetaMaskOnboarding();
const onboardButton = document.getElementById('metamaskConnect');
let accounts;
let nonce = 0;
async function getSignedData(publicAddress, jsonData) {
const signedData = await window.ethereum.request({
method: 'eth_signTypedData_v3',
params: [publicAddress, JSON.stringify(jsonData)]
});
console.log(signedData);
return signedData
}
const connectButton = async () => {
if (!MetaMaskOnboarding.isMetaMaskInstalled()) {
// onboardButton.innerText = 'Click here to install MetaMask!';
onboardButton.onclick = () => {
document.getElementById('metamaskConnect').classList.add('is-loading');
onboardButton.disabled = true;
onboarding.startOnboarding();
};
} else if (accounts && accounts.length > 0) {
document.getElementById('metamaskConnect').classList.remove('is-loading');
onboardButton.disabled = true;
onboarding.stopOnboarding();
} else {
onboardButton.onclick = async () => {
let userExists;
const allAccounts = await window.ethereum.request({
method: 'eth_requestAccounts',
});
await fetch('{{ url_for("api.user_exists") }}?public_address=' + allAccounts[0])
.then((resp) => resp.json())
.then(function(data) {
if (!data['success']) {
console.log('error checking user_exists!')
return
}
console.log(data);
nonce = data['nonce'];
})
const msg = 'Authentication request from SuchWowX app! Verifying message with nonce ' + nonce
const signedData = await window.ethereum.request({
method: 'personal_sign',
params: [msg, allAccounts[0]]
});
console.log(`Signing data with msg "${msg}", address "${allAccounts[0]}", signed data: ${signedData}`)
await fetch('{{ url_for("api.authenticate_metamask" ) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({
'signed_data': signedData,
'public_address': allAccounts[0],
'nonce': nonce,
'message': msg,
})
})
.then((resp) => resp.json())
.then(function(data) {
console.log(data)
if (data['success']) {
window.location.href = '/'
}
})
}
}
};
connectButton();
if (MetaMaskOnboarding.isMetaMaskInstalled()) {
window.ethereum.on('accountsChanged', (newAccounts) => {
accounts = newAccounts;
connectButton();
});
}
});
</script>
{% endif %}

@ -7,13 +7,14 @@
{% include 'includes/navbar.html' %} {% include 'includes/navbar.html' %}
{% if get_flashed_messages(with_categories=true) %} {% if get_flashed_messages(with_categories=true) %}
<div class="screen">
<p> <p>
</br>
You can run your own local IPFS instance and maintain metadata and artwork, You can run your own local IPFS instance and maintain metadata and artwork,
</br> </br>
try installing the software and running the following: try installing the software and running the following:
</p> </p>
<code>$ ipfs daemon</code> <code>$ ipfs daemon</code>
</div>
</br></br> </br></br>
{% endif %} {% endif %}
@ -25,14 +26,14 @@
<div class="card"> <div class="card">
<div class="card-image"> <div class="card-image">
<figure class="image"> <figure class="image">
<a href="{{ url_for('meta.meme', meme_id=meme.id) }}" up-preload up-follow=".container"> <a href="{{ url_for('meme.meme', meme_id=meme.id) }}" up-preload up-follow=".container">
{% if meme.upload_path.endswith('mp4') %} {% if meme.file_name.endswith('mp4') %}
<video class="img-fluid" {% if not request.MOBILE %}autoplay{% else %}controls{% endif %} muted loop> <video class="img-fluid" {% if not request.MOBILE %}autoplay{% else %}controls{% endif %} muted loop>
<source src="{{ url_for('meta.uploaded_file', filename=meme.upload_path) }}" type="video/mp4"> <source src="{{ url_for('meta.uploaded_file', filename=meme.file_name) }}" type="video/mp4">
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
{% else %} {% else %}
<img alt="{{ meme.title }}" src="{{ url_for('meta.uploaded_file', filename=meme.upload_path) }}" width="200px" class="img-fluid" style="" /> <img alt="{{ meme.title }}" src="{{ url_for('meta.uploaded_file', filename=meme.file_name) }}" width="200px" class="img-fluid" style="" />
{% endif %} {% endif %}
</a> </a>
</figure> </figure>

@ -10,18 +10,18 @@
{% if meme %} {% if meme %}
<div id="screen"> <div id="screen">
<div class="screen"> <div class="screen">
{% if meme.upload_path.endswith('mp4') %} {% if meme.file_name.endswith('mp4') %}
<video style="max-height: 60vh!important;max-width:100%;" {% if not request.MOBILE %}autoplay{% else %}controls{% endif %} muted loop> <video style="max-height: 60vh!important;max-width:100%;" {% if not request.MOBILE %}autoplay{% else %}controls{% endif %} muted loop>
<source src="{{ url_for('meta.uploaded_file', filename=meme.upload_path) }}" type="video/mp4"> <source src="{{ url_for('meta.uploaded_file', filename=meme.file_name) }}" type="video/mp4">
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
{% else %} {% else %}
<img src="{{ url_for('meta.uploaded_file', filename=meme.upload_path) }}" id="memeImage" /> <img src="{{ url_for('meta.uploaded_file', filename=meme.file_name) }}" id="memeImage" />
{% endif %} {% endif %}
</br> </br>
<p>Title: <strong>{{ meme.title }}</strong></p> <p>Title: <strong>{{ meme.title }}</strong></p>
<p>Description: <strong>{{ meme.description }}</strong></p> <p>Description: <strong>{{ meme.description }}</strong></p>
<p>Creator handle: <a href="{{ url_for('meta.creator', handle=meme.creator_handle) }}" up-layer="new" up-align="center">{{ meme.creator_handle }}</a></p> <p>Creator handle: <a up-layer="new" up-align="center">{{ meme.creator_handle }}</a></p>
<p>Meta IPFS: <a href="{{ config.IPFS_SERVER }}/ipfs/{{ meme.meta_ipfs_hash }}" target=_self up-preload up-follow=".container">{{ meme.meta_ipfs_hash }}</a></p> <p>Meta IPFS: <a href="{{ config.IPFS_SERVER }}/ipfs/{{ meme.meta_ipfs_hash }}" target=_self up-preload up-follow=".container">{{ meme.meta_ipfs_hash }}</a></p>
<p>Meme IPFS: <a href="{{ config.IPFS_SERVER }}/ipfs/{{ meme.meme_ipfs_hash }}" target=_self up-preload up-follow=".container">{{ meme.meme_ipfs_hash }}</a></p> <p>Meme IPFS: <a href="{{ config.IPFS_SERVER }}/ipfs/{{ meme.meme_ipfs_hash }}" target=_self up-preload up-follow=".container">{{ meme.meme_ipfs_hash }}</a></p>
<p>Meme ID: <code>{{ meme }}</code></p> <p>Meme ID: <code>{{ meme }}</code></p>

@ -10,9 +10,9 @@
<p class="subtitle"> <p class="subtitle">
Memes. <strong>Interplanetary</strong>! Memes. <strong>Interplanetary</strong>!
</p> </p>
<a class="button" href="{{ url_for('meta.index') }}" up-preload up-follow=".container">Go Back</a> <a class="button" href="{{ url_for('meme.index') }}" up-preload up-follow=".container">Go Back</a>
<form method="POST" enctype="multipart/form-data" class="site-form" id="memeUpload" action="{{ url_for('meta.new') }}" style="padding-top:1.5em;"> <form method="POST" enctype="multipart/form-data" class="site-form" id="memeUpload" action="{{ url_for('meme.publish') }}" style="padding-top:1.5em;">
<div class="field"> <div class="field">
<label class="label">File</label> <label class="label">File</label>
<div class="control"> <div class="control">
Loading…
Cancel
Save