🚧 Added Proof of Work on image-upload and more
This commit is contained in:
@@ -30,6 +30,7 @@ pillow = ">=9.1.1"
|
|||||||
authlib = "*"
|
authlib = "*"
|
||||||
httpx = "*"
|
httpx = "*"
|
||||||
itsdangerous = "*"
|
itsdangerous = "*"
|
||||||
|
puremagic = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
coverage = "*"
|
coverage = "*"
|
||||||
|
|||||||
Generated
+16
-8
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "259996d9979f6434bbd2a43c1146e10b485bca6df5fdb9d37319883800c56e33"
|
"sha256": "fe59e46268415d1b9088e7e27b599d88913b762a99d940336ec2e01e8e122378"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -250,11 +250,11 @@
|
|||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7",
|
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
|
||||||
"sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"
|
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2022.5.18.1"
|
"version": "==2022.6.15"
|
||||||
},
|
},
|
||||||
"cffi": {
|
"cffi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -883,6 +883,14 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.9.3"
|
"version": "==2.9.3"
|
||||||
},
|
},
|
||||||
|
"puremagic": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3d5df26cc7ec9aebbf842a09115a2fa85dc59ea6414fa568572c44775d796cbc",
|
||||||
|
"sha256:40e32752827f2d0cea7e6f11454fab2b4bd440582af83d8a47bf403ab42364fa"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.14"
|
||||||
|
},
|
||||||
"pyasn1": {
|
"pyasn1": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||||
@@ -1084,11 +1092,11 @@
|
|||||||
},
|
},
|
||||||
"setuptools": {
|
"setuptools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5a844ad6e190dccc67d6d7411d119c5152ce01f7c76be4d8a1eaa314501bba77",
|
"sha256:990a4f7861b31532871ab72331e755b5f14efbe52d336ea7f6118144dd478741",
|
||||||
"sha256:bf8a748ac98b09d32c9a64a995a6b25921c96cc5743c1efa82763ba80ff54e91"
|
"sha256:c1848f654aea2e3526d17fc3ce6aeaa5e7e24e66e645b5be2171f3f6b4e5a178"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==62.4.0"
|
"version": "==62.6.0"
|
||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1750,7 +1758,7 @@
|
|||||||
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||||
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
|
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '3.11'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==2.0.1"
|
"version": "==2.0.1"
|
||||||
},
|
},
|
||||||
"virtualenv": {
|
"virtualenv": {
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env python2.3
|
||||||
|
"""Implement Hashcash version 1 protocol in Python
|
||||||
|
+-------------------------------------------------------+
|
||||||
|
| Written by David Mertz; released to the Public Domain |
|
||||||
|
+-------------------------------------------------------+
|
||||||
|
|
||||||
|
Double spend database not implemented in this module, but stub
|
||||||
|
for callbacks is provided in the 'check()' function
|
||||||
|
|
||||||
|
The function 'check()' will validate hashcash v1 and v0 tokens, as well as
|
||||||
|
'generalized hashcash' tokens generically. Future protocol version are
|
||||||
|
treated as generalized tokens (should a future version be published w/o
|
||||||
|
this module being correspondingly updated).
|
||||||
|
|
||||||
|
A 'generalized hashcash' is implemented in the '_mint()' function, with the
|
||||||
|
public function 'mint()' providing a wrapper for actual hashcash protocol.
|
||||||
|
The generalized form simply finds a suffix that creates zero bits in the
|
||||||
|
hash of the string concatenating 'challenge' and 'suffix' without specifying
|
||||||
|
any particular fields or delimiters in 'challenge'. E.g., you might get:
|
||||||
|
|
||||||
|
>>> from hashcash import mint, _mint
|
||||||
|
>>> mint('foo', bits=16)
|
||||||
|
'1:16:040922:foo::+ArSrtKd:164b3'
|
||||||
|
>>> _mint('foo', bits=16)
|
||||||
|
'9591'
|
||||||
|
>>> import hashlib
|
||||||
|
>>> hashlib.sha1(('foo9591').encode()).hexdigest()
|
||||||
|
'0000de4c9b27cec9b20e2094785c1c58eaf23948'
|
||||||
|
>>> hashlib.sha1(('1:16:040922:foo::+ArSrtKd:164b3').encode()).hexdigest()
|
||||||
|
'0000a9fe0c6db2efcbcab15157735e77c0877f34'
|
||||||
|
|
||||||
|
Notice that '_mint()' behaves deterministically, finding the same suffix
|
||||||
|
every time it is passed the same arguments. 'mint()' incorporates a random
|
||||||
|
salt in stamps (as per the hashcash v.1 protocol).
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from string import ascii_letters
|
||||||
|
from math import ceil, floor
|
||||||
|
import hashlib
|
||||||
|
from random import choice
|
||||||
|
from time import strftime, localtime, time
|
||||||
|
|
||||||
|
|
||||||
|
class HCNotValid(BaseException):
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
DAYS = 60 * 60 * 24 # Seconds in a day
|
||||||
|
tries = [0] # Count hashes performed for benchmark
|
||||||
|
|
||||||
|
|
||||||
|
def mint(resource, bits=20, now=None, ext="", saltchars=8, stamp_seconds=False):
|
||||||
|
"""Mint a new hashcash stamp for 'resource' with 'bits' of collision
|
||||||
|
|
||||||
|
20 bits of collision is the default.
|
||||||
|
|
||||||
|
'ext' lets you add your own extensions to a minted stamp. Specify an
|
||||||
|
extension as a string of form 'name1=2,3;name2;name3=var1=2,2,val'
|
||||||
|
FWIW, urllib.urlencode(dct).replace('&',';') comes close to the
|
||||||
|
hashcash extension format.
|
||||||
|
|
||||||
|
'saltchars' specifies the length of the salt used; this version defaults
|
||||||
|
8 chars, rather than the C version's 16 chars. This still provides about
|
||||||
|
17 million salts per resource, per timestamp, before birthday paradox
|
||||||
|
collisions occur. Really paranoid users can use a larger salt though.
|
||||||
|
|
||||||
|
'stamp_seconds' lets you add the option time elements to the datestamp.
|
||||||
|
If you want more than just day, you get all the way down to seconds,
|
||||||
|
even though the spec also allows hours/minutes without seconds.
|
||||||
|
"""
|
||||||
|
ver = "1"
|
||||||
|
now = now or time()
|
||||||
|
if stamp_seconds:
|
||||||
|
ts = strftime("%y%m%d%H%M%S", localtime(now))
|
||||||
|
else:
|
||||||
|
ts = strftime("%y%m%d", localtime(now))
|
||||||
|
challenge = "%s:" * 6 % (ver, bits, ts, resource, ext, _salt(saltchars))
|
||||||
|
return challenge + _mint(challenge, bits)
|
||||||
|
|
||||||
|
|
||||||
|
def _salt(l):
|
||||||
|
"Return a random string of length 'l'"
|
||||||
|
alphabet = ascii_letters + "+/="
|
||||||
|
return "".join([choice(alphabet) for _ in [None] * l])
|
||||||
|
|
||||||
|
|
||||||
|
def _mint(challenge, bits):
|
||||||
|
"""Answer a 'generalized hashcash' challenge'
|
||||||
|
|
||||||
|
Hashcash requires stamps of form 'ver:bits:date:res:ext:rand:counter'
|
||||||
|
This internal function accepts a generalized prefix 'challenge',
|
||||||
|
and returns only a suffix that produces the requested SHA leading zeros.
|
||||||
|
|
||||||
|
NOTE: Number of requested bits is rounded up to the nearest multiple of 4
|
||||||
|
"""
|
||||||
|
counter = 0
|
||||||
|
hex_digits = int(ceil(bits / 4.0))
|
||||||
|
zeros = "0" * hex_digits
|
||||||
|
while 1:
|
||||||
|
digest = hashlib.sha1((challenge + hex(counter)[2:]).encode()).hexdigest()
|
||||||
|
if digest[:hex_digits] == zeros:
|
||||||
|
tries[0] = counter
|
||||||
|
return hex(counter)[2:]
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
|
||||||
|
def check(stamp, resource=None, bits=None, check_expiration=None, ds_callback=None):
|
||||||
|
"""Check whether a stamp is valid
|
||||||
|
|
||||||
|
Optionally, the stamp may be checked for a specific resource, and/or
|
||||||
|
it may require a minimum bit value, and/or it may be checked for
|
||||||
|
expiration, and/or it may be checked for double spending.
|
||||||
|
|
||||||
|
If 'check_expiration' is specified, it should contain the number of
|
||||||
|
seconds old a date field may be. Indicating days might be easier in
|
||||||
|
many cases, e.g.
|
||||||
|
|
||||||
|
>>> from hashcash import DAYS
|
||||||
|
>>> check(stamp, check_expiration=28*DAYS)
|
||||||
|
|
||||||
|
NOTE: Every valid (version 1) stamp must meet its claimed bit value
|
||||||
|
NOTE: Check floor of 4-bit multiples (overly permissive in acceptance)
|
||||||
|
"""
|
||||||
|
if stamp.startswith("0:"): # Version 0
|
||||||
|
try:
|
||||||
|
date, res, suffix = stamp[2:].split(":")
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
if resource is not None and resource != res:
|
||||||
|
return False
|
||||||
|
elif check_expiration is not None:
|
||||||
|
good_until = strftime("%y%m%d%H%M%S", localtime(time() - check_expiration))
|
||||||
|
if date < good_until:
|
||||||
|
return False
|
||||||
|
elif callable(ds_callback) and ds_callback(stamp):
|
||||||
|
return False
|
||||||
|
elif type(bits) is not int:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
hex_digits = int(floor(bits / 4))
|
||||||
|
return hashlib.sha1((stamp).encode()).hexdigest().startswith("0" * hex_digits)
|
||||||
|
elif stamp.startswith("1:"): # Version 1
|
||||||
|
try:
|
||||||
|
claim, date, res, ext, rand, counter = stamp[2:].split(":")
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
if resource is not None and resource != res:
|
||||||
|
return False
|
||||||
|
elif type(bits) is int and bits > int(claim):
|
||||||
|
return False
|
||||||
|
elif check_expiration is not None:
|
||||||
|
good_until = strftime("%y%m%d%H%M%S", localtime(time() - check_expiration))
|
||||||
|
if date < good_until:
|
||||||
|
return False
|
||||||
|
elif callable(ds_callback) and ds_callback(stamp):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
hex_digits = int(floor(int(claim) / 4))
|
||||||
|
return hashlib.sha1((stamp).encode()).hexdigest().startswith("0" * hex_digits)
|
||||||
|
else: # Unknown ver or generalized hashcash
|
||||||
|
if type(bits) is not int:
|
||||||
|
return True
|
||||||
|
elif resource is not None and stamp.find(resource) < 0:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
hex_digits = int(floor(bits / 4))
|
||||||
|
return hashlib.sha1((stamp).encode()).hexdigest().startswith("0" * hex_digits)
|
||||||
@@ -11,18 +11,21 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from classquiz.config import settings, redis, storage, meilisearch
|
from classquiz.config import settings, redis, storage, meilisearch
|
||||||
from classquiz.db.models import Quiz, QuizInput, User
|
from classquiz.db.models import Quiz, QuizInput, User
|
||||||
|
import puremagic
|
||||||
from classquiz.auth import get_current_user
|
from classquiz.auth import get_current_user
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from classquiz.helpers import get_meili_data
|
from classquiz.helpers import get_meili_data, check_hashcash
|
||||||
from classquiz.storage.errors import DeletionFailedError
|
from classquiz.storage.errors import DeletionFailedError
|
||||||
|
|
||||||
settings = settings()
|
settings = settings()
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
allowed_image_extensions = [".gif", ".jpg", ".jpeg", ".png", ".svg", ".webp"]
|
||||||
|
|
||||||
|
|
||||||
class InitEditorResponse(BaseModel):
|
class InitEditorResponse(BaseModel):
|
||||||
token: str
|
token: str
|
||||||
@@ -60,21 +63,44 @@ async def init_editor(edit: bool, quiz_id: Optional[UUID] = None, user: User = D
|
|||||||
return InitEditorResponse(token=edit_id)
|
return InitEditorResponse(token=edit_id)
|
||||||
|
|
||||||
|
|
||||||
|
class GetPowData(BaseModel):
|
||||||
|
data: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/pow", response_model=GetPowData)
|
||||||
|
async def get_pow_data(edit_id: str):
|
||||||
|
session_data = await redis.get(f"edit_session:{edit_id}")
|
||||||
|
if session_data is None:
|
||||||
|
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||||
|
random_str = os.urandom(8).hex()
|
||||||
|
await redis.set(f"edit_session:{edit_id}:pow", random_str, ex=3800)
|
||||||
|
return GetPowData(data=random_str)
|
||||||
|
|
||||||
|
|
||||||
class UploadImageReturn(BaseModel):
|
class UploadImageReturn(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
|
||||||
|
|
||||||
@router.post("/image", response_model=UploadImageReturn)
|
@router.post("/image", response_model=UploadImageReturn)
|
||||||
async def upload_image(edit_id: str, file: UploadFile = File()):
|
async def upload_image(edit_id: str, pow_data: str, file: UploadFile = File()):
|
||||||
|
print(pow_data)
|
||||||
session_data = await redis.get(f"edit_session:{edit_id}")
|
session_data = await redis.get(f"edit_session:{edit_id}")
|
||||||
|
pow_data_server = await redis.get(f"edit_session:{edit_id}:pow")
|
||||||
|
if pow_data_server is None:
|
||||||
|
print("1")
|
||||||
|
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||||
|
if not check_hashcash(pow_data, pow_data_server):
|
||||||
|
print("2")
|
||||||
|
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||||
if session_data is None:
|
if session_data is None:
|
||||||
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||||
|
file_bytes = await file.read()
|
||||||
|
lol = puremagic.magic_string(file_bytes)
|
||||||
|
print(lol)
|
||||||
session_data = EditSessionData.parse_raw(session_data)
|
session_data = EditSessionData.parse_raw(session_data)
|
||||||
file_name = f"{session_data.quiz_id}--{uuid.uuid4()}"
|
file_name = f"{session_data.quiz_id}--{uuid.uuid4()}"
|
||||||
print("Uploading...")
|
# await storage.upload(file_name=file_name, file_data=file_bytes)
|
||||||
await storage.upload(file_name=file_name, file_data=await file.read())
|
# await redis.lpush(f"edit_session:{edit_id}:images", file_name)
|
||||||
print("Finished Upload")
|
|
||||||
await redis.lpush(f"edit_session:{edit_id}:images", file_name)
|
|
||||||
return UploadImageReturn(id=file_name)
|
return UploadImageReturn(id=file_name)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ async def create_user(user: route_user, background_task: BackgroundTasks) -> Use
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
user.verify_key = str(os.urandom(16).hex())
|
user.verify_key = str(os.urandom(16).hex())
|
||||||
res = await User.objects.filter((User.email == user.email) | (User.username == user.username)).all()
|
res = await User.objects.filter((User.email == user.email) | (User.username == user.username)).all()
|
||||||
|
|
||||||
if len(res) != 0:
|
if len(res) != 0:
|
||||||
raise HTTPException(status_code=409, detail="User already exists")
|
raise HTTPException(status_code=409, detail="User already exists")
|
||||||
|
|
||||||
@@ -162,9 +161,8 @@ class ForgotPassword(BaseModel):
|
|||||||
@router.post("/forgot-password")
|
@router.post("/forgot-password")
|
||||||
async def forgotten_password(forgot_password: ForgotPassword, background_task: BackgroundTasks):
|
async def forgotten_password(forgot_password: ForgotPassword, background_task: BackgroundTasks):
|
||||||
user = await User.objects.filter(email=forgot_password.email, verified=True).get_or_none()
|
user = await User.objects.filter(email=forgot_password.email, verified=True).get_or_none()
|
||||||
if user is None:
|
if user is not None:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
background_task.add_task(send_forgotten_password_email, email=user.email)
|
||||||
background_task.add_task(send_forgotten_password_email, email=user.email)
|
|
||||||
return {"message": "Password reset email sent"}
|
return {"message": "Password reset email sent"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class DetaStorage:
|
|||||||
if response.status == 201:
|
if response.status == 201:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
|
print(response.status, await response.json())
|
||||||
raise SavingFailedError
|
raise SavingFailedError
|
||||||
|
|
||||||
async def delete(self, file_names: [str]) -> None:
|
async def delete(self, file_names: [str]) -> None:
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"@uppy/xhr-upload": "^2.1.2",
|
"@uppy/xhr-upload": "^2.1.2",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
"cssnano": "^5.1.11",
|
"cssnano": "^5.1.11",
|
||||||
"eslint": "^8.17.0",
|
"eslint": "^8.17.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
|||||||
Generated
+9
@@ -27,6 +27,7 @@ specifiers:
|
|||||||
'@uppy/xhr-upload': ^2.1.2
|
'@uppy/xhr-upload': ^2.1.2
|
||||||
autoprefixer: ^10.4.7
|
autoprefixer: ^10.4.7
|
||||||
cookie: ^0.5.0
|
cookie: ^0.5.0
|
||||||
|
crypto-js: ^4.1.1
|
||||||
cssnano: ^5.1.11
|
cssnano: ^5.1.11
|
||||||
eslint: ^8.17.0
|
eslint: ^8.17.0
|
||||||
eslint-config-prettier: ^8.5.0
|
eslint-config-prettier: ^8.5.0
|
||||||
@@ -89,6 +90,7 @@ devDependencies:
|
|||||||
'@uppy/xhr-upload': 2.1.2_@uppy+core@2.3.1
|
'@uppy/xhr-upload': 2.1.2_@uppy+core@2.3.1
|
||||||
autoprefixer: 10.4.7_postcss@8.4.14
|
autoprefixer: 10.4.7_postcss@8.4.14
|
||||||
cookie: 0.5.0
|
cookie: 0.5.0
|
||||||
|
crypto-js: 4.1.1
|
||||||
cssnano: 5.1.11_postcss@8.4.14
|
cssnano: 5.1.11_postcss@8.4.14
|
||||||
eslint: 8.17.0
|
eslint: 8.17.0
|
||||||
eslint-config-prettier: 8.5.0_eslint@8.17.0
|
eslint-config-prettier: 8.5.0_eslint@8.17.0
|
||||||
@@ -1562,6 +1564,13 @@ packages:
|
|||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/crypto-js/4.1.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/css-declaration-sorter/6.3.0_postcss@8.4.14:
|
/css-declaration-sorter/6.3.0_postcss@8.4.14:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getLocalization } from '$lib/i18n';
|
import { getLocalization } from '$lib/i18n';
|
||||||
import * as yup from 'yup';
|
import { mint } from '$lib/hashcash';
|
||||||
import { dataSchema } from '$lib/yupSchemas';
|
import { dataSchema } from '$lib/yupSchemas';
|
||||||
import type { EditorData, Question, Answer } from './quiz_types';
|
import type { EditorData, Question, Answer } from './quiz_types';
|
||||||
import Sidebar from '$lib/editor/sidebar.svelte';
|
import Sidebar from '$lib/editor/sidebar.svelte';
|
||||||
import SettingsCard from '$lib/editor/settings-card.svelte';
|
import SettingsCard from '$lib/editor/settings-card.svelte';
|
||||||
import QuizCard from '$lib/editor/card.svelte';
|
import QuizCard from '$lib/editor/card.svelte';
|
||||||
import Spinner from './Spinner.svelte';
|
import Spinner from './Spinner.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const { t } = getLocalization();
|
const { t } = getLocalization();
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@
|
|||||||
};
|
};
|
||||||
let edit_id;
|
let edit_id;
|
||||||
let confirm_to_leave = true;
|
let confirm_to_leave = true;
|
||||||
|
let pow_data;
|
||||||
|
|
||||||
const getEditID = async () => {
|
const getEditID = async () => {
|
||||||
let res;
|
let res;
|
||||||
@@ -82,7 +84,7 @@
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
edit_id = json.token;
|
edit_id = json.token;
|
||||||
console.log(edit_id, json);
|
setPOWdata();
|
||||||
} else {
|
} else {
|
||||||
alert('Error!');
|
alert('Error!');
|
||||||
}
|
}
|
||||||
@@ -110,7 +112,6 @@
|
|||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
console.log('Hier');
|
|
||||||
confirm_to_leave = false;
|
confirm_to_leave = false;
|
||||||
console.log(confirm_to_leave);
|
console.log(confirm_to_leave);
|
||||||
window.location.href = '/overview';
|
window.location.href = '/overview';
|
||||||
@@ -118,6 +119,13 @@
|
|||||||
alert('Error');
|
alert('Error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const setPOWdata = async () => {
|
||||||
|
const res = await fetch(`/api/v1/editor/pow?edit_id=${edit_id}`);
|
||||||
|
const data = (await res.json()).data;
|
||||||
|
console.log(data);
|
||||||
|
pow_data = await mint(data);
|
||||||
|
console.log(pow_data);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:beforeunload={confirmUnload} />
|
<svelte:window on:beforeunload={confirmUnload} />
|
||||||
@@ -167,7 +175,7 @@
|
|||||||
{#if selected_question === -1}
|
{#if selected_question === -1}
|
||||||
<SettingsCard bind:data />
|
<SettingsCard bind:data />
|
||||||
{:else}
|
{:else}
|
||||||
<QuizCard bind:data bind:selected_question bind:edit_id />
|
<QuizCard bind:data bind:selected_question bind:edit_id bind:pow_data />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
export let data: EditorData;
|
export let data: EditorData;
|
||||||
export let selected_question: number;
|
export let selected_question: number;
|
||||||
export let edit_id: string;
|
export let edit_id: string;
|
||||||
|
export let pow_data;
|
||||||
const empty_answer: Answer = {
|
const empty_answer: Answer = {
|
||||||
right: false,
|
right: false,
|
||||||
answer: ''
|
answer: ''
|
||||||
@@ -67,6 +68,8 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if pow_data === undefined}
|
||||||
|
<Spinner />
|
||||||
{:else}
|
{:else}
|
||||||
{#await import('$lib/editor/uploader.svelte')}
|
{#await import('$lib/editor/uploader.svelte')}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -77,6 +80,7 @@
|
|||||||
bind:edit_id
|
bind:edit_id
|
||||||
bind:data
|
bind:data
|
||||||
bind:selected_question
|
bind:selected_question
|
||||||
|
bind:pow_data
|
||||||
/>
|
/>
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
export let edit_id: string;
|
export let edit_id: string;
|
||||||
export let data: EditorData;
|
export let data: EditorData;
|
||||||
export let selected_question: number;
|
export let selected_question: number;
|
||||||
|
export let pow_data;
|
||||||
|
|
||||||
|
console.log(pow_data);
|
||||||
const uppy = new Uppy()
|
const uppy = new Uppy()
|
||||||
.use(DropTarget, {
|
.use(DropTarget, {
|
||||||
target: document.body
|
target: document.body
|
||||||
@@ -34,7 +36,7 @@
|
|||||||
quality: 0.6
|
quality: 0.6
|
||||||
})
|
})
|
||||||
.use(XHRUpload, {
|
.use(XHRUpload, {
|
||||||
endpoint: `/api/v1/editor/image?edit_id=${edit_id}`
|
endpoint: `/api/v1/editor/image?edit_id=${edit_id}&pow_data=${pow_data}`
|
||||||
});
|
});
|
||||||
const props = { inline: true };
|
const props = { inline: true };
|
||||||
let image_id;
|
let image_id;
|
||||||
@@ -42,6 +44,7 @@
|
|||||||
image_id = response.body.id;
|
image_id = response.body.id;
|
||||||
});
|
});
|
||||||
uppy.on('complete', (res) => {
|
uppy.on('complete', (res) => {
|
||||||
|
console.log(pow_data);
|
||||||
data.questions[
|
data.questions[
|
||||||
selected_question
|
selected_question
|
||||||
].image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
|
].image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
const gen_salt = (l: number): string => {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
const charLength = chars.length;
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < l; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * charLength));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mint = async (
|
||||||
|
resource: string,
|
||||||
|
bits = 19,
|
||||||
|
now = null,
|
||||||
|
ext = '',
|
||||||
|
saltchars = 8,
|
||||||
|
stamp_seconds = false
|
||||||
|
): Promise<string> => {
|
||||||
|
const ver = '1';
|
||||||
|
let ts;
|
||||||
|
if (stamp_seconds) {
|
||||||
|
ts = DateTime.now().toFormat('yyMMddHHmmss');
|
||||||
|
} else {
|
||||||
|
ts = DateTime.now().toFormat('yyMMdd');
|
||||||
|
}
|
||||||
|
const hex_digits = Math.ceil(bits / 4);
|
||||||
|
const zeros = '0'.repeat(hex_digits);
|
||||||
|
const salt = gen_salt(saltchars);
|
||||||
|
const challenge = `${ver}:${bits}:${ts}:${resource}:${ext}:${salt}`;
|
||||||
|
let digest: string;
|
||||||
|
let counter = 0;
|
||||||
|
let result: string;
|
||||||
|
while (true) {
|
||||||
|
const data = new TextEncoder().encode(`${challenge}:${counter.toString(16)}`);
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const digest = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
if (digest.slice(0, hex_digits) == zeros) {
|
||||||
|
result = counter.toString(16);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
return `${challenge}:${result}`;
|
||||||
|
};
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
import LandingPromo from '$lib/landing/landing-promo.svelte';
|
import LandingPromo from '$lib/landing/landing-promo.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { mint } from '$lib/hashcash';
|
||||||
|
|
||||||
const { t } = getLocalization();
|
const { t } = getLocalization();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user