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 @@
>
+ 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 @@