diff --git a/backend/src/api/protein.py b/backend/src/api/protein.py index 73f7517e..02513c9f 100644 --- a/backend/src/api/protein.py +++ b/backend/src/api/protein.py @@ -8,7 +8,7 @@ from fastapi.exceptions import HTTPException from ..api_types import ProteinEntry, UploadBody, UploadError, EditBody, CamelModel -from ..auth import requiresAuthentication +from ..auth import requires_authentication from ..tmalign import tm_align from io import BytesIO from fastapi import APIRouter @@ -193,7 +193,7 @@ def get_protein_entry(protein_name: str): # TODO: add permissions so only the creator can delete not just anyone @router.delete("/protein/entry/{protein_name:str}", response_model=None) def delete_protein_entry(protein_name: str, req: Request): - requiresAuthentication(req) + requires_authentication(req) # Todo, have a meaningful error if the delete fails with Database() as db: # remove protein @@ -210,7 +210,8 @@ def delete_protein_entry(protein_name: str, req: Request): @router.post("/protein/upload/png", response_model=None) -def upload_protein_png(body: UploadPNGBody): +def upload_protein_png(body: UploadPNGBody, req: Request): + requires_authentication(req) with Database() as db: try: query = """UPDATE proteins SET thumbnail = %s WHERE name = %s""" @@ -222,7 +223,7 @@ def upload_protein_png(body: UploadPNGBody): # None return means success @router.post("/protein/upload", response_model=UploadError | None) def upload_protein_entry(body: UploadBody, req: Request): - requiresAuthentication(req) + requires_authentication(req) body.name = format_protein_name(body.name) # check that the name is not already taken in the DB @@ -290,7 +291,7 @@ def edit_protein_entry(body: EditBody, req: Request): # check that the name is not already taken in the DB # TODO: check if permission so we don't have people overriding other people's names - requiresAuthentication(req) + requires_authentication(req) try: # replace spaces in the name with underscores body.old_name = format_protein_name(body.old_name) diff --git a/backend/src/api/users.py b/backend/src/api/users.py index 5d96d4d8..b3c1ca39 100644 --- a/backend/src/api/users.py +++ b/backend/src/api/users.py @@ -3,7 +3,7 @@ from passlib.hash import bcrypt from ..api_types import LoginBody, LoginResponse from ..db import Database -from ..auth import generateAuthToken +from ..auth import generate_auth_token router = APIRouter() @@ -32,7 +32,7 @@ def login(body: LoginBody): return LoginResponse(token="", error="Invalid Email or Password") # Generates the token and returns - token = generateAuthToken(email, admin) + token = generate_auth_token(email, admin) log.warn("Giving token:", token) return LoginResponse(token=token, error="") diff --git a/backend/src/auth.py b/backend/src/auth.py index 9d491449..ec1e5d93 100644 --- a/backend/src/auth.py +++ b/backend/src/auth.py @@ -9,7 +9,7 @@ secret_key = "SuperSecret" -def generateAuthToken(userId, admin): +def generate_auth_token(userId, admin): payload = { "email": userId, "admin": admin, @@ -18,13 +18,13 @@ def generateAuthToken(userId, admin): return jwt.encode(payload, secret_key, algorithm="HS256") -def authenticateToken(token): +def authenticate_token(token): # Return the decoded token if it's valid. try: # Valid token is always is in the form "Bearer [token]", so we need to slice off the "Bearer" portion. sliced_token = token[7:] log.warn(sliced_token) - decoded = jwt.decode(sliced_token, secret_key, algorithms="HS256") + decoded = jwt.decode(sliced_token, secret_key, algorithms=["HS256"]) log.warn("Valid token") log.warn(decoded) return decoded @@ -36,8 +36,13 @@ def authenticateToken(token): # Use this function with a request if you want. -def requiresAuthentication(req: Request): - userInfo = authenticateToken(req.headers["authorization"]) +def requires_authentication(req: Request): + # no header at all + if "authorization" not in req.headers: + raise HTTPException(status_code=403, detail="Unauthorized") + + # verify token is good if provided + userInfo = authenticate_token(req.headers["authorization"]) if not userInfo or not userInfo.get("admin"): log.error("Unauthorized User") raise HTTPException(status_code=403, detail="Unauthorized") diff --git a/frontend/src/lib/ListProteins.svelte b/frontend/src/lib/ListProteins.svelte index c6ecfeb5..dca14a8b 100644 --- a/frontend/src/lib/ListProteins.svelte +++ b/frontend/src/lib/ListProteins.svelte @@ -22,6 +22,7 @@ >
thumbnail + import { onDestroy } from "svelte"; import { PDBeMolstarPlugin } from "../../venome-molstar/lib"; + import { loseWebGLContext } from "./venomeMolstarUtils"; export let url = ""; export let format = "pdb"; @@ -7,11 +9,11 @@ export let binary = false; export let width = 500; export let height = 500; + let m: PDBeMolstarPlugin; let divEl: HTMLDivElement; async function render() { - // @ts-ignore - const m = new PDBeMolstarPlugin(); // loaded through app.html + m = new PDBeMolstarPlugin(); // some bs for the whole thing to rerender. TODO: fix this. divEl.innerHTML = ""; const div = document.createElement("div"); @@ -33,6 +35,11 @@ }); } + onDestroy(() => { + loseWebGLContext(divEl.querySelector("canvas")!); + m.plugin.dispose(); + }); + $: { if (url && divEl) { render(); diff --git a/frontend/src/lib/venomeMolstarUtils.ts b/frontend/src/lib/venomeMolstarUtils.ts new file mode 100644 index 00000000..d3342928 --- /dev/null +++ b/frontend/src/lib/venomeMolstarUtils.ts @@ -0,0 +1,76 @@ +import { BACKEND_URL } from "./backend"; +import { PDBeMolstarPlugin } from "../../venome-molstar/lib"; +import type { InitParams } from "../../venome-molstar/lib/spec"; + +export async function screenshotMolstar(initParams: Partial) { + const { div, molstar } = await renderHeadless(initParams); + const imgData = await getPreview(molstar); + + // cleanup + loseWebGLContext(div.querySelector("canvas")!); + molstar.plugin.dispose(); + div.remove(); + + return imgData; +} + +export function loseWebGLContext(canvas: HTMLCanvasElement) { + const gl = canvas.getContext("webgl"); + if (gl) { + const loseContext = gl.getExtension("WEBGL_lose_context"); + if (loseContext) { + loseContext.loseContext(); + } + } +} + +async function getPreview(m: PDBeMolstarPlugin, checkDelayMs = 25) { + const blankPreview = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAFACAYAAADNkKWqAAAAAXNSR0IArs4c6QAACUJJREFUeF7t1AENADAMAsHNv2iWzMZfHXA03G07jgABAkGBawCDrYtMgMAXMIAegQCBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AwAM7Tfx9MOLD/gAAAABJRU5ErkJggg=="; + while (true) { + const imgData = m.plugin.helpers.viewportScreenshot + ?.getPreview()! + .canvas.toDataURL()!; + if (imgData !== blankPreview) { + return imgData; + } + await delay(checkDelayMs); + } +} + +async function renderHeadless(initParams: Partial) { + const molstar = new PDBeMolstarPlugin(); + const div = document.createElement("div"); + await molstar.render(div, initParams); + return { div, molstar }; +} + +async function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export const defaultInitParams = (name: string): Partial => ({ + customData: { + url: `${BACKEND_URL}/protein/pdb/${name}`, + format: "pdb", + binary: false, + }, + subscribeEvents: false, + bgColor: { + r: 255, + g: 255, + b: 255, + }, + selectInteraction: false, + alphafoldView: true, + reactive: false, + sequencePanel: false, + hideControls: true, + hideCanvasControls: [ + "animation", + "expand", + "selection", + "controlToggle", + "controlInfo", + ], +}); diff --git a/frontend/src/routes/Edit.svelte b/frontend/src/routes/Edit.svelte index dd0c655c..9840e17a 100644 --- a/frontend/src/routes/Edit.svelte +++ b/frontend/src/routes/Edit.svelte @@ -184,7 +184,7 @@ on:click={async () => { setToken(); await Backend.deleteProteinEntry(urlId); - navigate("/"); + navigate("/search"); }}>Delete Protein Entry
diff --git a/frontend/src/routes/ForceUploadThumbnails.svelte b/frontend/src/routes/ForceUploadThumbnails.svelte index 399a16f6..343da8dd 100644 --- a/frontend/src/routes/ForceUploadThumbnails.svelte +++ b/frontend/src/routes/ForceUploadThumbnails.svelte @@ -1,11 +1,10 @@ -
-
Uploading {uploaded} of {proteinsNeedingPng.length}
- preview + {proteinsNeedingPng[uploaded]?.name} +
+
diff --git a/frontend/src/routes/Upload.svelte b/frontend/src/routes/Upload.svelte index f64e3f5a..eff1a872 100644 --- a/frontend/src/routes/Upload.svelte +++ b/frontend/src/routes/Upload.svelte @@ -1,4 +1,8 @@