Skip to content

Commit

Permalink
🌒♻️ ↝ Merge pull request #21 from Signal-K/sta-24-setup-authenticatio…
Browse files Browse the repository at this point in the history
…n-using-moralis

🌒♻️ ↝ Flask, Lens, Moralis authentication + Read/Write, graphql interactivity
#16 , Signal-K/Silfur#24
  • Loading branch information
Gizmotronn authored Jan 1, 2023
2 parents 62ae57d + 2ec4eb7 commit 1224daa
Show file tree
Hide file tree
Showing 67 changed files with 7,563 additions and 12,135 deletions.
Binary file modified .DS_Store
Binary file not shown.
3 changes: 2 additions & 1 deletion Server/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
env
.vscode
.vscode
.env
28 changes: 0 additions & 28 deletions Server/Home/package.json

This file was deleted.

10,976 changes: 0 additions & 10,976 deletions Server/Home/yarn.lock

This file was deleted.

4 changes: 3 additions & 1 deletion Server/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ name = "pypi"
moralis = "*"
flask = "*"
flask_cors = "*"
thirdweb-sdk = "*"
python-dotenv = "*"

[dev-packages]

[requires]
python_version = "3.10"
python_version = "3.9.9"
1,163 changes: 1,158 additions & 5 deletions Server/Pipfile.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make sure to initialise the Flask app with `pipenv` and run the command `export FLASK_APP=app.py`
115 changes: 67 additions & 48 deletions Server/app.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,76 @@
from flask import Flask
from flask import request
from moralis import auth
from flask_cors import CORS
from flask import Flask, request, make_response, jsonify
from thirdweb.types import LoginPayload
from thirdweb import ThirdwebSDK
from datetime import datetime, timedelta
import os

# Flask application setup
app = Flask(__name__)
CORS(app)
apiKey = "kJfYYpmMmfKhvaWMdD3f3xMMb24B4MHBDDVrfjslkKgTilvMgdwr1bwKUr8vWdHH" # Move to env

# Authentication routes -> move to auth.py later
# Request a challenge when a user attempts to connect their wallet
@app.route('/requestChallenge', methods=['GET'])
def reqChallenge():
args = request.args # Fetch the arguments from the request

# Get request body -> compare with data from Moralis
body = {
"domain": "sailors.skinetics.tech",
"chainId": args.get("chainId"),
"address": args.get("address"),
"statement": "Please confirm authentication by signing this transaction",
"uri": "https://ipfs.skinetics.tech/auth/1...",
"expirationTime": "2023-01-01T00:00:00.000Z",
"notBefore": "2020-01-01T00:00:00.000Z",
"resources": ['https://docs.skinetics.tech/crypto/auth/signing'],
"timeout": 30,
}

# Deliver the result to Moralis client
result = auth.challenge.request_challenge_evm(
api_key=apiKey,
body=body,

@app.route('/', methods=["GET"])
def index():
return "Hello World"

@app.route('/login', methods=['POST'])
def login():
private_key = os.environ.get("PRIVATE_KEY")

if not private_key:
print("Missing PRIVATE_KEY environment variable")
return "Wallet private key not set", 400

sdk = ThirdwebSDK.from_private_key(private_key, 'mumbai') # Initialise the sdk using the wallet and on mumbai testnet chain
payload = LoginPayload.from_json(request.json['payload'])

# Generate access token using signed payload
domain = 'sailors.skinetics.tech'
token = sdk.auth.generate_auth_token(domain, payload)

res = make_response()
res.set_cookie(
'access_token',
token,
path='/',
httponly=True,
secure=True,
samesite='strict',
)
return res, 200

return result
@app.route('/authenticate', methods=['POST'])
def authenticate():
private_key = os.environ.get("PRIVATE_KEY")

if not private_key:
print("Missing PRIVATE_KEY environment variable")
return "Wallet private key not set", 400

# Verify signature from user
@app.route('/verifyChallenge', methods=['GET'])
def verifyChallenge():
args = request.args
sdk = ThirdwebSDK.from_private_key(private_key, 'mumbai')

body = { # Request body
"message": args.get("message"),
"signature": args.get("signature"),
},
# Get access token from cookies
token = request.cookies.get('access_token')
if not token:
return 'Unauthorised', 401

domain = 'sailors.skinetics.tech'

result = auth.challenge.verify_challenge_evm(
api_key=apiKey,
body=body,
),
try:
address = sdk.auth.authenticate(domain, token)
except:
return "Unauthorized", 401

print(jsonify(address))
return jsonify(address), 200

return result
@app.route('/logout', methods=['POST'])
def logout():
res = make_response()
res.set_cookie(
'access_token',
'none',
expires=datetime.utcnow() + timedelta(second = 5)
)
return res, 200

# Initialising Flask application
if __name__ == '__main__':
app.run(host='127.0.0.1', port='5000', debug=True)
@app.route('/helloworld')
def helloworld():
return address
File renamed without changes.
File renamed without changes.
16 changes: 16 additions & 0 deletions Server/frontend/components/Navbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ConnectButton } from "web3uikit";
import Link from "next/link";

export default function Navbar() {
return (
<ul>
<Link href='/lens/'><li>Home</li></Link>
<Link href='/lens/write-post'><li>Create Proposal</li></Link>
<li>
<div>
<ConnectButton moralisAuth={false} />
</div>
</li>
</ul>
)
};
10 changes: 10 additions & 0 deletions Server/frontend/components/PostContent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ReactMarkdown from "react-markdown";

export default function PostContent({ post }) {
return (
<div>
<h1>{post.metadata.name}</h1>
<ReactMarkdown>{post.metadata.content}</ReactMarkdown>
</div>
)
}
27 changes: 27 additions & 0 deletions Server/frontend/components/PostFeed.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Link from "next/link";

export default function PostFeed({ posts }) {
return (
<div className="p-2">
{posts
? posts.map((post) => <PostItem post={post} key={post.id} />)
: null}
</div>
);
}

function PostItem({ post }) {
let imageURL;
if (post.metadata.image) { // IPFS gateway (URI/url)
imageURL = post.metadata.image.replace("ipfs://", 'https://ipfs.io/ipfs'); // Replace ipfs link with a regular http ref.uri
}

return (
<div>
<Link href={`/posts/${post.id}`}>
<img src={imageURL} />
<h2>{post.metadata.name}</h2>
</Link>
</div>
)
}
130 changes: 130 additions & 0 deletions Server/frontend/components/WritePost.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useForm } from "react-hook-form";
import { useLensContext } from '../context/lensContext';
import {
createContentMetadata,
getCreatePostQuery, // Compare to https://github.com/PatrickAlphaC/lens-blog
lensClient,
} from "../constants/lensConstants";
import { useWeb3Contract } from "react-moralis";
import lensAbi from '../contracts/lensABI.json';
import {
lensHub,
networkConfig,
TRUE_BYTES,
} from "../constants/contractConstants";

const PINATA_PIN_ENDPOINT = 'https://api.pinata.cloud/pinning/pinJSONToIPFS';

async function pinMetadataToPinata (
metadata,
contentName,
pinataApiKey,
pinataApiSecret
) {
console.log('pinning metadata to pinata');
const data = JSON.stringify({
pinataMetadata: { name: contentName },
pinataContent: metadata,
});
const config = {
method: "POST",
headers: {
"Content-Type": 'application/json',
pinata_api_key: pinataApiKey,
pinata_secret_api_key: pinataApiSecret,
},
body: data,
};
const response = await fetch(PINATA_PIN_ENDPOINT, config);
const ipfsHash = (await response.json()).ipfsHash;
console.log(`Stored content metadata with ${ipfsHash}`);
return ipfsHash;
}

function PostForm () {
const { profileId, token } = useLensContext();
const { register, errors, handleSubmit, formState, reset, watch } = useForm({
mode: 'onChange',
});
const { runContractFunction } = useWeb3Contract();

const publishPost = async function ({ content, contentName, imageUri, imageType, pinataApiKey, pinataApiSecret }) {
let fullContentUri;
const contentMetadata = createContentMetadata(content, contentName, imageUri, imageType);
const metadataIpfsHash = await pinMetadataToPinata(contentMetadata, contentName, pinataApiKey, pinataApiSecret);
fullContentUri = `ipfs://${metadataIpfsHash}`;
console.log(fullContentUri);

// Post IPFS hash to Lens/blockchain
const transactionParameters = [
profileId,
fullContentUri,
'0x23b9467334bEb345aAa6fd1545538F3d54436e96', // Free collect module contract address on Polygon (for now, all posts will be able to be collected without a fee).
TRUE_BYTES,
'0x17317F96f0C7a845FFe78c60B10aB15789b57Aaa', // Follower only reference module
];
console.log(transactionParameters);
const transactionOptions = {
abi: lensAbi,
contractAddress: '0xDb46d1Dc155634FbC732f92E853b10B288AD5a1d', // Lens Hub proxy contract address
functionName: 'post',
params: {
vars: transactionParameters,
},
};

await runContractFunction({
params: transactionOptions,
onError: (error) => console.log(error),
});

return (
"Hi"
)
};

return (
<form onSubmit = { handleSubmit( publishPost ) }>
<input placeholder="Publication title" name='contentName' {...register("contentName", {maxLength: 100, minLength: 1, required: true})} />
<textarea placeholder="Write your proposal in markdown" name='content' {...register('content', {
maxLength: 2500, minLength: 10, required: true
})} />
<input placeholder="(optional) Image URI" name='imageUri' {...register("imageUri", { // Feature request -> ability to add multiple images? (Markdown ![]() styling as a temporary work around?)
maxLength: 100, minLength: 1, required: false
})} />
<input placeholder="(optional) Image type" name='imageType' {...register("imageType", {
maxLength: 100, minLength: 1, required: false
})} />
<input
placeholder="(optional) Pinata.cloud API Key"
name="pinataApiKey"
{...register("pinataApiKey", { // Feature request -> This should be saved into a user's account (via Supabase) and retrieved whenever this page is loaded
maxLength: 100,
minLength: 1,
required: false,
})}
/>
<input
placeholder="(optional) Pinata.cloud API Secret"
name="pinataApiSecret"
{...register("pinataApiSecret", { // Probably would need to implement Moralis <==> Supabase auth process (unless we switch back to Thirdweb SDK for auth & signing) so that the user address is -> Supabase
maxLength: 100,
minLength: 1,
required: false,
})}
/>
{errors ? <div>{errors.content?.message}</div> : <div></div> }
{profileId && token ? (
<button type='submit'>Publish</button>
) : <div>You need to sign in to make a post</div>}
</form>
)
}

export default function WritePost () {
return (
<div><PostForm /></div>
)
}

// Send form components to Flask -> Supabase.
Loading

0 comments on commit 1224daa

Please sign in to comment.