wire up metamask authentication

main
lza_menace 3 years ago
parent 73800d2a69
commit 2a9b1995ce

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

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

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

@ -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
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 '<script>window.history.back()</script>'
return redirect(url_for('meta.index'))
return redirect(url_for('meme.index'))
if "file" in request.files:
if form_err:
return '<script>window.history.back()</script>'
@ -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/<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>')
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/<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>
{% if request.path == '/' %}
<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 %}
<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 %}
</div>

@ -1,5 +1,5 @@
<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> -->
{% with messages = get_flashed_messages(with_categories=true) %}
@ -18,3 +18,92 @@
</script>
{% endif %}
{% 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' %}
{% if get_flashed_messages(with_categories=true) %}
<div class="screen">
<p>
</br>
You can run your own local IPFS instance and maintain metadata and artwork,
</br>
try installing the software and running the following:
</p>
<code>$ ipfs daemon</code>
</div>
</br></br>
{% endif %}
@ -25,14 +26,14 @@
<div class="card">
<div class="card-image">
<figure class="image">
<a href="{{ url_for('meta.meme', meme_id=meme.id) }}" up-preload up-follow=".container">
{% if meme.upload_path.endswith('mp4') %}
<a href="{{ url_for('meme.meme', meme_id=meme.id) }}" up-preload up-follow=".container">
{% if meme.file_name.endswith('mp4') %}
<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.
</video>
{% 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 %}
</a>
</figure>

@ -10,18 +10,18 @@
{% if meme %}
<div id="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>
<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.
</video>
{% 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 %}
</br>
<p>Title: <strong>{{ meme.title }}</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>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>

@ -10,9 +10,9 @@
<p class="subtitle">
Memes. <strong>Interplanetary</strong>!
</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">
<label class="label">File</label>
<div class="control">
Loading…
Cancel
Save