🔒 Fixed some security issues. Please upgrade!
This commit is contained in:
@@ -102,4 +102,6 @@ meilisearch = MeiliSearch.Client(settings().meilisearch_url)
|
|||||||
|
|
||||||
ALLOWED_TAGS_FOR_QUIZ = ["b", "strong", "i", "em", "small", "mark", "del", "sub", "sup"]
|
ALLOWED_TAGS_FOR_QUIZ = ["b", "strong", "i", "em", "small", "mark", "del", "sub", "sup"]
|
||||||
|
|
||||||
|
ALLOWED_MIME_TYPES = ["image/png", "video/mp4", "image/jpeg", "image/gif", "image/webp"]
|
||||||
|
|
||||||
server_regex = rf"^{re.escape(settings().root_address)}/api/v1/storage/download/.{{36}}--.{{36}}$"
|
server_regex = rf"^{re.escape(settings().root_address)}/api/v1/storage/download/.{{36}}--.{{36}}$"
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
|||||||
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!")
|
||||||
session_data = EditSessionData.parse_raw(session_data)
|
session_data = EditSessionData.parse_raw(session_data)
|
||||||
quiz_input.title = html.unescape(bleach.clean(quiz_input.title, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True))
|
quiz_input.title = bleach.clean(quiz_input.title, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True)
|
||||||
quiz_input.description = html.unescape(bleach.clean(quiz_input.description, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True))
|
quiz_input.description = bleach.clean(quiz_input.description, tags=ALLOWED_TAGS_FOR_QUIZ, strip=True)
|
||||||
if quiz_input.background_color is not None:
|
if quiz_input.background_color is not None:
|
||||||
quiz_input.background_color = html.unescape(bleach.clean(quiz_input.background_color, tags=[], strip=True))
|
quiz_input.background_color = bleach.clean(quiz_input.background_color, tags=[], strip=True)
|
||||||
|
|
||||||
for i, question in enumerate(quiz_input.questions):
|
for i, question in enumerate(quiz_input.questions):
|
||||||
if question.type == QuizQuestionType.ABCD:
|
if question.type == QuizQuestionType.ABCD:
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ router = APIRouter()
|
|||||||
router.include_router(shares_router, prefix="/shares")
|
router.include_router(shares_router, prefix="/shares")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create")
|
@router.post("/create", response_model_exclude={"user": ...})
|
||||||
async def create_quiztivity(data: QuizTivityInput, user: User = Depends(get_current_user)) -> QuizTivity:
|
async def create_quiztivity(data: QuizTivityInput, user: User = Depends(get_current_user)) -> QuizTivity:
|
||||||
quiztivity = QuizTivity.parse_obj({**data.dict(), "user": user, "id": uuid4(), "created_at": datetime.now()})
|
quiztivity = QuizTivity.parse_obj({**data.dict(), "user": user, "id": uuid4(), "created_at": datetime.now()})
|
||||||
return await quiztivity.save()
|
return await quiztivity.save()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{uuid}")
|
@router.get("/{uuid}", response_model_exclude={"user": ...})
|
||||||
async def get_quiztivity(uuid: UUID) -> QuizTivity:
|
async def get_quiztivity(uuid: UUID) -> QuizTivity:
|
||||||
quiztivity = await QuizTivity.objects.get_or_none(id=uuid)
|
quiztivity = await QuizTivity.objects.get_or_none(id=uuid)
|
||||||
if quiztivity is None:
|
if quiztivity is None:
|
||||||
@@ -31,7 +31,7 @@ async def get_quiztivity(uuid: UUID) -> QuizTivity:
|
|||||||
return quiztivity
|
return quiztivity
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{uuid}")
|
@router.put("/{uuid}", response_model_exclude={"user": ...})
|
||||||
async def put_quiztivity(data: QuizTivityInput, uuid: UUID, user: User = Depends(get_current_user)) -> QuizTivity:
|
async def put_quiztivity(data: QuizTivityInput, uuid: UUID, user: User = Depends(get_current_user)) -> QuizTivity:
|
||||||
quiztivity = await QuizTivity.objects.get_or_none(id=uuid, user=user)
|
quiztivity = await QuizTivity.objects.get_or_none(id=uuid, user=user)
|
||||||
if quiztivity is None:
|
if quiztivity is None:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from fastapi.responses import StreamingResponse, RedirectResponse
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from classquiz.auth import get_current_user
|
from classquiz.auth import get_current_user
|
||||||
from classquiz.config import settings, storage, arq
|
from classquiz.config import settings, storage, arq, ALLOWED_MIME_TYPES
|
||||||
from classquiz.db.models import User, StorageItem, PublicStorageItem, UpdateStorageItem, PrivateStorageItem
|
from classquiz.db.models import User, StorageItem, PublicStorageItem, UpdateStorageItem, PrivateStorageItem
|
||||||
from classquiz.helpers import check_image_string
|
from classquiz.helpers import check_image_string
|
||||||
from classquiz.storage.errors import DownloadingFailedError
|
from classquiz.storage.errors import DownloadingFailedError
|
||||||
@@ -119,10 +119,11 @@ async def download_file_head(file_name: str) -> Response:
|
|||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def upload_file(file: UploadFile = File(), user: User = Depends(get_current_user)) -> PublicStorageItem:
|
async def upload_file(file: UploadFile = File(), user: User = Depends(get_current_user)) -> PublicStorageItem:
|
||||||
|
if file.content_type not in ALLOWED_MIME_TYPES:
|
||||||
|
raise HTTPException(status_code=422, detail="Unsupported")
|
||||||
if user.storage_used > settings.free_storage_limit:
|
if user.storage_used > settings.free_storage_limit:
|
||||||
raise HTTPException(status_code=409, detail="Storage limit reached")
|
raise HTTPException(status_code=409, detail="Storage limit reached")
|
||||||
file_id = uuid4()
|
file_id = uuid4()
|
||||||
|
|
||||||
size = 0
|
size = 0
|
||||||
file_obj = StorageItem(
|
file_obj = StorageItem(
|
||||||
id=file_id,
|
id=file_id,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from fastapi import APIRouter, Response, HTTPException, Request, Depends, status
|
|||||||
from fastapi.background import BackgroundTasks
|
from fastapi.background import BackgroundTasks
|
||||||
from fastapi.responses import JSONResponse, RedirectResponse, PlainTextResponse
|
from fastapi.responses import JSONResponse, RedirectResponse, PlainTextResponse
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
import html
|
|
||||||
|
|
||||||
from jose import jwt, JWTError
|
from jose import jwt, JWTError
|
||||||
|
|
||||||
@@ -78,7 +77,7 @@ async def create_user(user: RouteUser, background_task: BackgroundTasks) -> User
|
|||||||
raise HTTPException(status_code=409, detail="User already exists")
|
raise HTTPException(status_code=409, detail="User already exists")
|
||||||
|
|
||||||
user.password = get_password_hash(user.password)
|
user.password = get_password_hash(user.password)
|
||||||
user.username = html.unescape(bleach.clean(user.username, tags=[], strip=True))
|
user.username = bleach.clean(user.username, tags=[], strip=True)
|
||||||
if len(user.username) == 32:
|
if len(user.username) == 32:
|
||||||
return JSONResponse({"details": "Username mustn't be 32 characters long"}, 400)
|
return JSONResponse({"details": "Username mustn't be 32 characters long"}, 400)
|
||||||
await user.save()
|
await user.save()
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import os
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import pyotp
|
import pyotp
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from classquiz.auth import get_current_user
|
from classquiz.auth import get_current_user, verify_password
|
||||||
from classquiz.db.models import User
|
from classquiz.db.models import User
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -20,10 +20,16 @@ class GetBackupCodeResponse(BaseModel):
|
|||||||
code: str
|
code: str
|
||||||
|
|
||||||
|
|
||||||
@router.get("/backup_code", response_model=GetBackupCodeResponse)
|
class RequirePasswordForAction(BaseModel):
|
||||||
async def get_backup_code(user: User = Depends(get_current_user)):
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/backup_code", response_model=GetBackupCodeResponse)
|
||||||
|
async def get_backup_code(data: RequirePasswordForAction, user: User = Depends(get_current_user)):
|
||||||
backup_code = os.urandom(32).hex()
|
backup_code = os.urandom(32).hex()
|
||||||
user = await User.objects.get(id=user.id)
|
user = await User.objects.get(id=user.id)
|
||||||
|
if not verify_password(data.password, user.password):
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid")
|
||||||
user.backup_code = backup_code
|
user.backup_code = backup_code
|
||||||
await user.update()
|
await user.update()
|
||||||
return GetBackupCodeResponse(code=backup_code)
|
return GetBackupCodeResponse(code=backup_code)
|
||||||
@@ -31,11 +37,14 @@ async def get_backup_code(user: User = Depends(get_current_user)):
|
|||||||
|
|
||||||
class SetRequirePassword(BaseModel):
|
class SetRequirePassword(BaseModel):
|
||||||
require_password: bool
|
require_password: bool
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
@router.post("/require_password", response_model=SetRequirePassword)
|
@router.post("/require_password", response_model=SetRequirePassword)
|
||||||
async def set_require_password(data: SetRequirePassword, user: User = Depends(get_current_user)):
|
async def set_require_password(data: SetRequirePassword, user: User = Depends(get_current_user)):
|
||||||
user = await User.objects.get(id=user.id)
|
user = await User.objects.get(id=user.id)
|
||||||
|
if not verify_password(data.password, user.password):
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid")
|
||||||
user.require_password = data.require_password
|
user.require_password = data.require_password
|
||||||
await user.update()
|
await user.update()
|
||||||
return data
|
return data
|
||||||
@@ -47,8 +56,10 @@ class SetTotpUpResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/totp", response_model=SetTotpUpResponse)
|
@router.post("/totp", response_model=SetTotpUpResponse)
|
||||||
async def set_totp_up(user: User = Depends(get_current_user)):
|
async def set_totp_up(data: RequirePasswordForAction, user: User = Depends(get_current_user)):
|
||||||
user = await User.objects.get(id=user.id)
|
user = await User.objects.get(id=user.id)
|
||||||
|
if not verify_password(data.password, user.password):
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid")
|
||||||
user.totp_secret = pyotp.random_base32()
|
user.totp_secret = pyotp.random_base32()
|
||||||
url = pyotp.totp.TOTP(user.totp_secret).provisioning_uri(
|
url = pyotp.totp.TOTP(user.totp_secret).provisioning_uri(
|
||||||
name=urllib.parse.quote(user.username), issuer_name="ClassQuiz"
|
name=urllib.parse.quote(user.username), issuer_name="ClassQuiz"
|
||||||
@@ -71,7 +82,9 @@ async def get_totp_status(user: User = Depends(get_current_user)):
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/totp")
|
@router.delete("/totp")
|
||||||
async def disable_totp(user: User = Depends(get_current_user)):
|
async def disable_totp(data: RequirePasswordForAction, user: User = Depends(get_current_user)):
|
||||||
user = await User.objects.get(id=user.id)
|
user = await User.objects.get(id=user.id)
|
||||||
|
if not verify_password(data.password, user.password):
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid")
|
||||||
user.totp_secret = None
|
user.totp_secret = None
|
||||||
await user.update()
|
await user.update()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from webauthn.helpers.cose import COSEAlgorithmIdentifier
|
from webauthn.helpers.cose import COSEAlgorithmIdentifier
|
||||||
|
|
||||||
from classquiz.auth import get_current_user
|
from classquiz.auth import get_current_user, verify_password
|
||||||
from classquiz.db.models import User, FidoCredentials
|
from classquiz.db.models import User, FidoCredentials
|
||||||
from classquiz.config import redis, settings
|
from classquiz.config import redis, settings
|
||||||
|
|
||||||
@@ -28,9 +28,15 @@ settings = settings()
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/add_key", response_model=PublicKeyCredentialCreationOptions)
|
class RequirePasswordForAction(BaseModel):
|
||||||
async def request_add_key_data(user: User = Depends(get_current_user)):
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/add_key_init", response_model=PublicKeyCredentialCreationOptions)
|
||||||
|
async def request_add_key_data(data: RequirePasswordForAction, user: User = Depends(get_current_user)):
|
||||||
user = await User.objects.select_related("fidocredentialss").get(id=user.id)
|
user = await User.objects.select_related("fidocredentialss").get(id=user.id)
|
||||||
|
if not verify_password(data.password, user.password):
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid")
|
||||||
options = generate_registration_options(
|
options = generate_registration_options(
|
||||||
rp_id=urllib.parse.urlparse(settings.root_address).hostname,
|
rp_id=urllib.parse.urlparse(settings.root_address).hostname,
|
||||||
rp_name="ClassQuiz",
|
rp_name="ClassQuiz",
|
||||||
@@ -86,7 +92,9 @@ async def list_security_keys(user: User = Depends(get_current_user)):
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/key/{key_id}")
|
@router.delete("/key/{key_id}")
|
||||||
async def delete_security_key(key_id: int, user: User = Depends(get_current_user)):
|
async def delete_security_key(data: RequirePasswordForAction, key_id: int, user: User = Depends(get_current_user)):
|
||||||
|
if not verify_password(data.password, user.password):
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid")
|
||||||
key = await FidoCredentials.objects.get_or_none(pk=key_id, user=user.id)
|
key = await FidoCredentials.objects.get_or_none(pk=key_id, user=user.id)
|
||||||
if key is None:
|
if key is None:
|
||||||
raise HTTPException(status_code=404, detail="Key not found")
|
raise HTTPException(status_code=404, detail="Key not found")
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
"felte": "^1.2.7",
|
"felte": "^1.2.7",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
|
"i18next": "^22.4.15",
|
||||||
"i18next-browser-languagedetector": "^7.0.1",
|
"i18next-browser-languagedetector": "^7.0.1",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"jws": "^4.0.0",
|
"jws": "^4.0.0",
|
||||||
@@ -104,8 +105,5 @@
|
|||||||
"vite-plugin-iso-import": "^1.0.0",
|
"vite-plugin-iso-import": "^1.0.0",
|
||||||
"yup": "^1.1.1"
|
"yup": "^1.1.1"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module"
|
||||||
"dependencies": {
|
|
||||||
"i18next": "^22.4.15"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
-6
@@ -4,11 +4,6 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
dependencies:
|
|
||||||
i18next:
|
|
||||||
specifier: ^22.4.15
|
|
||||||
version: 22.4.15
|
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@beyonk/svelte-mapbox':
|
'@beyonk/svelte-mapbox':
|
||||||
specifier: ^9.0.5
|
specifier: ^9.0.5
|
||||||
@@ -169,6 +164,9 @@ devDependencies:
|
|||||||
highlight.js:
|
highlight.js:
|
||||||
specifier: ^11.7.0
|
specifier: ^11.7.0
|
||||||
version: 11.7.0
|
version: 11.7.0
|
||||||
|
i18next:
|
||||||
|
specifier: ^22.4.15
|
||||||
|
version: 22.4.15
|
||||||
i18next-browser-languagedetector:
|
i18next-browser-languagedetector:
|
||||||
specifier: ^7.0.1
|
specifier: ^7.0.1
|
||||||
version: 7.0.1
|
version: 7.0.1
|
||||||
@@ -284,6 +282,7 @@ packages:
|
|||||||
engines: { node: '>=6.9.0' }
|
engines: { node: '>=6.9.0' }
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.13.11
|
regenerator-runtime: 0.13.11
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@beyonk/svelte-mapbox@9.0.5(svelte@3.58.0):
|
/@beyonk/svelte-mapbox@9.0.5(svelte@3.58.0):
|
||||||
resolution:
|
resolution:
|
||||||
@@ -3490,7 +3489,7 @@ packages:
|
|||||||
}
|
}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.21.0
|
'@babel/runtime': 7.21.0
|
||||||
dev: false
|
dev: true
|
||||||
|
|
||||||
/ieee754@1.2.1:
|
/ieee754@1.2.1:
|
||||||
resolution:
|
resolution:
|
||||||
@@ -5065,6 +5064,7 @@ packages:
|
|||||||
{
|
{
|
||||||
integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
|
integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
|
||||||
}
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/require-directory@2.1.1:
|
/require-directory@2.1.1:
|
||||||
resolution:
|
resolution:
|
||||||
|
|||||||
@@ -223,7 +223,7 @@
|
|||||||
"abcd_description": "Nur eine Antwort kann ausgewählt werden",
|
"abcd_description": "Nur eine Antwort kann ausgewählt werden",
|
||||||
"voting_description": "Antworten geben keine Punkte",
|
"voting_description": "Antworten geben keine Punkte",
|
||||||
"order_description": "Antworten müssen in die richtige Reihenfolge gebracht werden",
|
"order_description": "Antworten müssen in die richtige Reihenfolge gebracht werden",
|
||||||
"text_description": "Spieler können text eingeben",
|
"text_description": "Spieler können Text eingeben",
|
||||||
"range_description": "Ein Zahlenbereich kann mit einem Schieberegler ausgewählt werden",
|
"range_description": "Ein Zahlenbereich kann mit einem Schieberegler ausgewählt werden",
|
||||||
"check_choice_description": "Alle richtigen Antworten müssen für Punkte ausgewählt werden",
|
"check_choice_description": "Alle richtigen Antworten müssen für Punkte ausgewählt werden",
|
||||||
"need_more_help": "Benötigst du mehr Hilfe?",
|
"need_more_help": "Benötigst du mehr Hilfe?",
|
||||||
|
|||||||
@@ -36,18 +36,25 @@ SPDX-License-Identifier: MPL-2.0
|
|||||||
if (!browser || user_data?.require_password === undefined) {
|
if (!browser || user_data?.require_password === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const pw = require_password()
|
||||||
const res = await fetch('/api/v1/users/2fa/require_password', {
|
const res = await fetch('/api/v1/users/2fa/require_password', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ require_password: user_data?.require_password })
|
body: JSON.stringify({ require_password: user_data?.require_password, password: pw })
|
||||||
});
|
});
|
||||||
user_data.require_password = (await res.json()).require_password;
|
user_data.require_password = (await res.json()).require_password;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const require_password = (): string => {
|
||||||
|
return prompt("Please enter your password to continue")
|
||||||
|
}
|
||||||
|
|
||||||
const add_security_key = async () => {
|
const add_security_key = async () => {
|
||||||
const res1 = await fetch('/api/v1/users/webauthn/add_key');
|
const pw = require_password()
|
||||||
|
const res1 = await fetch('/api/v1/users/webauthn/add_key_init', {method: "POST", body: JSON.stringify({password: pw}), headers: { 'Content-Type': 'application/json' }});
|
||||||
|
if (res1.status === 401) {alert("Password probably wrong"); return}
|
||||||
if (!res1.ok) {
|
if (!res1.ok) {
|
||||||
throw Error('Response not ok');
|
throw Error('Response not ok');
|
||||||
}
|
}
|
||||||
@@ -74,29 +81,36 @@ SPDX-License-Identifier: MPL-2.0
|
|||||||
};
|
};
|
||||||
|
|
||||||
const remove_security_key = async (key_id: number) => {
|
const remove_security_key = async (key_id: number) => {
|
||||||
await fetch(`/api/v1/users/webauthn/key/${key_id}`, { method: 'DELETE' });
|
const pw = require_password()
|
||||||
|
const res = await fetch(`/api/v1/users/webauthn/key/${key_id}`, { method: 'DELETE', body: JSON.stringify({password: pw}), headers: { 'Content-Type': 'application/json' } });
|
||||||
|
if (res.status === 401) {alert("Password probably wrong"); return}
|
||||||
data = get_data();
|
data = get_data();
|
||||||
};
|
};
|
||||||
|
|
||||||
const disable_totp = async () => {
|
const disable_totp = async () => {
|
||||||
await fetch(`/api/v1/users/2fa/totp`, { method: 'DELETE' });
|
const pw = require_password()
|
||||||
|
const res = await fetch(`/api/v1/users/2fa/totp`, { method: 'DELETE', body: JSON.stringify({password: pw}), headers: { 'Content-Type': 'application/json' } });
|
||||||
|
if (res.status === 401) {alert("Password probably wrong"); return}
|
||||||
data = get_data();
|
data = get_data();
|
||||||
};
|
};
|
||||||
|
|
||||||
const enable_totp = async () => {
|
const enable_totp = async () => {
|
||||||
const res = await fetch('/api/v1/users/2fa/totp', { method: 'POST' });
|
const pw = require_password()
|
||||||
|
const res = await fetch('/api/v1/users/2fa/totp', { method: 'POST', body: JSON.stringify({password: pw}), headers: { 'Content-Type': 'application/json' } });
|
||||||
|
if (res.status === 401) {alert("Password probably wrong"); return}
|
||||||
data = get_data();
|
data = get_data();
|
||||||
totp_data = await res.json();
|
totp_data = await res.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
const get_backup_code = async () => {
|
const get_backup_code = async () => {
|
||||||
|
const pw = require_password()
|
||||||
if (!confirm('If you continue, your old backup-code will be removed.')) {
|
if (!confirm('If you continue, your old backup-code will be removed.')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await fetch('/api/v1/users/2fa/backup_code');
|
const res = await fetch('/api/v1/users/2fa/backup_code', { method: 'POST', body: JSON.stringify({password: pw}), headers: { 'Content-Type': 'application/json' } });
|
||||||
|
if (res.status === 401) {alert("Password probably wrong"); return}
|
||||||
backup_code = (await res.json()).code;
|
backup_code = (await res.json()).code;
|
||||||
};
|
};
|
||||||
$: console.log(user_data?.require_password, 'hello');
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await data}
|
{#await data}
|
||||||
@@ -116,11 +130,12 @@ SPDX-License-Identifier: MPL-2.0
|
|||||||
</div>
|
</div>
|
||||||
<div class="h-full w-full">
|
<div class="h-full w-full">
|
||||||
<h2 class="text-center text-2xl">{$t('security_settings.activate_2fa')}</h2>
|
<h2 class="text-center text-2xl">{$t('security_settings.activate_2fa')}</h2>
|
||||||
<div class="flex h-full w-full justify-center flex-col">
|
<div class="flex h-full w-full justify-center flex-col" class:pointer-events-none={!totp_activated} class:grayscale={!totp_activated} class:opacity-50={!totp_activated}>
|
||||||
<div class="m-auto">
|
<div class="m-auto">
|
||||||
{#if user_data.require_password}
|
{#if user_data.require_password}
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
|
disabled={!totp_activated}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
user_data.require_password = !user_data.require_password;
|
user_data.require_password = !user_data.require_password;
|
||||||
save_password_required();
|
save_password_required();
|
||||||
@@ -142,6 +157,7 @@ SPDX-License-Identifier: MPL-2.0
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
|
disabled={!totp_activated}
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
user_data.require_password = !user_data.require_password;
|
user_data.require_password = !user_data.require_password;
|
||||||
|
|||||||
+10
-9
@@ -2,23 +2,24 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
CONTAINER_BIN=podman
|
||||||
|
|
||||||
run_tests() {
|
run_tests() {
|
||||||
pipenv run coverage run -m pytest -s -v --asyncio-mode=strict classquiz/tests
|
pipenv run coverage run -m pytest -s -v --asyncio-mode=strict classquiz/tests
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
docker container stop classquiz_db
|
$CONTAINER_BIN container stop classquiz_db
|
||||||
docker container stop test_redis
|
$CONTAINER_BIN container stop test_redis
|
||||||
docker container stop test_meili
|
$CONTAINER_BIN container stop test_meili
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
mkdir /tmp/storage
|
mkdir /tmp/storage
|
||||||
docker run --rm -d -p 6379:6379 --name test_redis redis:alpine
|
$CONTAINER_BIN run --rm -d -p 6379:6379 --name test_redis redis:alpine
|
||||||
docker run -it --rm -d -p 7700:7700 --name test_meili getmeili/meilisearch:v0.28.0
|
$CONTAINER_BIN run -it --rm -d -p 7700:7700 --name test_meili getmeili/meilisearch:v0.28.0
|
||||||
docker volume create classquiz_db_data
|
$CONTAINER_BIN volume create classquiz_db_data
|
||||||
docker run --name classquiz_db -p 5432:5432 --rm -d -e POSTGRES_PASSWORD=mysecretpassword -v classquiz_db_data:/var/lib/postgresql/data -e POSTGRES_DB=classquiz postgres
|
$CONTAINER_BIN run --name classquiz_db -p 5432:5432 --rm -d -e POSTGRES_PASSWORD=mysecretpassword -v classquiz_db_data:/var/lib/postgresql/data -e POSTGRES_DB=classquiz postgres
|
||||||
sleep 1
|
sleep 1
|
||||||
pipenv run alembic upgrade head
|
pipenv run alembic upgrade head
|
||||||
}
|
}
|
||||||
@@ -27,14 +28,14 @@ case $1 in
|
|||||||
+) init ;;
|
+) init ;;
|
||||||
-) stop ;;
|
-) stop ;;
|
||||||
a)
|
a)
|
||||||
docker volume rm classquiz_db_data
|
$CONTAINER_BIN volume rm classquiz_db_data
|
||||||
init
|
init
|
||||||
run_tests
|
run_tests
|
||||||
stop
|
stop
|
||||||
;;
|
;;
|
||||||
prepare)
|
prepare)
|
||||||
stop
|
stop
|
||||||
docker volume rm classquiz_db_data
|
$CONTAINER_BIN volume rm classquiz_db_data
|
||||||
init
|
init
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
Reference in New Issue
Block a user