mirror of
https://github.com/jwinterm/tubbymemes.git
synced 2026-10-17 05:21:28 +02:00
wire up metamask authentication
This commit is contained in:
@@ -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
|
||||||
|
|||||||
+17
-2
@@ -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
|
||||||
+51
-1
@@ -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">
|
||||||
Reference in New Issue
Block a user