Skip to content

Commit

Permalink
Merge branch 'color' into article-components
Browse files Browse the repository at this point in the history
  • Loading branch information
xnought committed Apr 18, 2024
2 parents 752c010 + c099aa2 commit b9acc0a
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 36 deletions.
19 changes: 19 additions & 0 deletions backend/src/api/protein.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,30 @@ def str_as_file_stream(input_str: str, filename_as: str) -> StreamingResponse:
)


def get_residue_bfactors(pdb: PDB):
chains = {}
for chain in pdb.structure.get_chains():
chains[chain.get_id()] = []
for r in chain.get_residues():
for a in r.get_atoms():
chains[chain.get_id()].append(a.bfactor)
break
return chains


"""
ENDPOINTS TODO: add the other protein types here instead of in api_types.py
"""


@router.get("/protein/pLDDT/{protein_name:str}", response_model=dict[str, list[float]])
def get_pLDDT_given_protein(protein_name: str):
if protein_name_found(protein_name):
pdb = parse_protein_pdb(protein_name, encoding="file")
return get_residue_bfactors(pdb)
return {}


class UploadPNGBody(CamelModel):
protein_name: str
base64_encoding: str
Expand Down
39 changes: 39 additions & 0 deletions backend/src/api/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,42 @@ def search_venome_similar(protein_name: str):
raise HTTPException(500, "Error getting protein descriptions")

return formatted


@router.get(
"/search/venome/similar/{protein_name:str}/{protein_compare:str}",
response_model=SimilarProtein,
)
def search_venome_similar_compare(protein_name: str, protein_compare: str):
target = stored_pdb_file_name(protein_compare)
# ignore the first since it's itself as the most similar
try:
similar = easy_search(
stored_pdb_file_name(protein_name),
target,
out_format="target,prob,evalue,qstart,qend",
) # qend,qstart refer to alignment
formatted = [
SimilarProtein(
name=name.rstrip(".pdb"),
prob=prob,
evalue=evalue,
qstart=qstart,
qend=qend,
alntmscore=0,
)
for [name, prob, evalue, qstart, qend] in similar
]
except Exception:
raise HTTPException(404, "Error in 'foldseek easy-search' command")

try:
# populate protein descriptions for the similar proteins
descriptions = get_descriptions([s.name for s in formatted])
if descriptions is not None:
for f, d in zip(formatted, descriptions):
f.description = d
except Exception:
raise HTTPException(500, "Error getting protein descriptions")

return formatted[0]
2 changes: 1 addition & 1 deletion backend/src/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def login(body: LoginBody):
if entry_sql is None or len(entry_sql) == 0:
return LoginResponse(token="", error="Invalid Email or Password")

# Grabs the stored hash and admin.
# Grabs the stored hash and admin status.
password_hash, admin = entry_sql[0]

# Returns "incorrect email/password" message if password is incorrect.
Expand Down
2 changes: 1 addition & 1 deletion backend/src/data/pdbAlphaFold/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
*.pdb
*.pdb
52 changes: 52 additions & 0 deletions docs/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Authentication
Venome uses a first-party authentication scheme.

## Login Flow
1. User goes to login page and provides username and password and presses "Log In". Client sends POST request to backend's */users/login* API endpoint. The request contains JSON with the username and password. See *submitForm()* in [`Login.svelte`](../frontend/src/routes/Login.svelte)
2. Backend verifies provided information against database's username and hashed/salted password If verified, returns a JSON Web Token (JWT) to the frontend, and if not verified, sends an error. See *login()* in [`users.py`](../backend/src/api/users.py)
3. Frontend, if the user is verified:
* Stores the JWT into the browser as a cookie. See *Cookies.set()* in [`Login.svelte`](../frontend/src/routes/Login.svelte)
* Sets *user* svelte store **loggedIn** attributes to true. See *$user.loggedIn = true* in [`Login.svelte`](../frontend/src/routes/Login.svelte)
* The JWT cookie and user store are used to access restricted functionality in the website and API. See [`Impelentation Tips`](#implementation-tips).
4. When the user reloads the website, the Frontend checks to see if they're logged in by looking at the browser cookie and sets the *user* store accordingly. See *onMount()* in [`Header.svelte`](../frontend/src/routes/Login.svelte)
5. When the user presses "log out", it sends them back to the login page. This clears the auth cookie from the browser and unsets the *user* store attributes. See *onMount()* in [`Login.svelte`](../frontend/src/routes/Login.svelte)

## Implementation Tips
There are a few functions we created to make it easy to lock elements and endpoints behind authentication.

### Backend: Locking an API call behind authentication
1. Import *requires_authentication()* from auth.py
2. Call *requires_authentication()* at the top of the API call you want to restrict.

In /backend/src/auth.py, *requires_authentication()* takes in a Request object as a parameter, checks if it has an authorization header, and validates the contained JWT against the database to determine if the user is an admin. If they aren't an admin, it raises an HTTP Exception; Otherwise, the API call proceeds as normal.

For an example, see the *upload_tutorial()* in [`tutorials.py`](../backend/src/api/tutorials.py).

### Frontend: Accessing a locked API call
1. import *setToken()* /lib/backend.ts
2. Call *setToken()* before making the restricted API call.

In /frontend/src/lib/backend.ts, *setToken()* reads the authentication JWT stored in the user's browser cookie, and sets the TOKEN header for outgoing HTTP requests to that token.

For an example, look at [`Edit.svelte`](../frontend/src/routes/Edit.svelte) and search for setToken.

### Frontend: Hiding pages or elements if the user isn't logged in
1. Check if *$user.loggedIn* is true or false.
2. Hide the element or redirect as needed.

To track whether a user is logged in for the purposes of hiding elements, we use a Svelte store called "user" (defined in [`user.ts`](../frontend/stores/user.ts)). This store has an attribute called **loggedIn.** The **loggedIn** attribute is set to *true* either when the user has just logged in, or if they open the website while they have an authentication cookie stored. The attribute is set to *false* when the user logs out and defaults to *false* if the site is reloaded.

Svelte provides an easy shorthand to interface with a svelte store; You can simply type in "$user.*attribute*" to look at the contents of any store attribute. The other user store attributes (*username*, *admin*, etc.) could be used in a similar way in the future, but we are not using them at this time.

You can see some examples of use-cases in [`Header.svelte`](../frontend/src/lib/Header.svelte) and [`Upload.svelte`](../frontend/src/routes/Upload.svelte); Just search for *$user*.

## Concerns and To-Dos
* Originally, we had a choice between using first-party or third-party authentication. We settled on using first-party authentication because it was quicker and easier to implement, but third-party authentication through something like Google would be a more secure choice. The Oauth 2 protocol (which services like Google use for third-party authentication) uses JWT-based authentication similar to what we did here, so some of the code we've already written could be adapted for use in third-party authentication.
* Our frontend and backend currently communicate over HTTP instead of HTTPS. This is mostly a concern where we send usernames and passwords (e.g. the /users/login API endpoint), but this should probably be changed even if switching to third-party authentication.
* The authentication token has an "exp" which lists a 24 hour expiration date (see generate_auth_token() in auth.py), but we don't use this anywhere. This can either be used in the future, or *probably* safely removed.
* Right now, we're only using the **loggedIn** attribute of the user store. While we do have attributes like **admin** and **username**, they are untested and probably have bugs.
* We implemented this authentication scheme with limited security experience. As such, there may be more flaws than listed here which we did not consider.

## Library References
* [`PyJWT`](https://github.com/jpadilla/pyjwt) for JWT generation and verification in the backend.
* [`js-cookie`](https://github.com/js-cookie/js-cookie) for securely storing JWTs in browser cookies in the frontend.
7 changes: 5 additions & 2 deletions frontend/src/lib/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
import Cookies from "js-cookie";
import ProteinIcon from "../lib/ProteinIcon.svelte";
// Checking if the user has a cookie.
// If they do, set user status for the whole site.
onMount(async () => {
/**
* Checking if the user has a cookie. If they do, set user svelte store loggin attribute.
* Done here because the header is loaded first, which means user can still directly navigate
* to restricted pages if they refresh while logged in.
*/
if (Cookies.get("auth")) {
$user.loggedIn = true;
}
Expand Down
32 changes: 29 additions & 3 deletions frontend/src/lib/Molstar.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<script lang="ts">
import { onDestroy } from "svelte";
import { PDBeMolstarPlugin } from "../../venome-molstar/lib";
import { loseWebGLContext } from "./venomeMolstarUtils";
import {
loseWebGLContext,
colorResidues,
type ChainColors,
} from "./venomeMolstarUtils";
import type { QueryParam } from "../../venome-molstar/lib/helpers";
export let url = "";
export let format = "pdb";
Expand All @@ -12,8 +17,9 @@
export let hideControls = true;
export let zIndex = 999;
export let spin = false;
export let chainColors: ChainColors = {};
let m: PDBeMolstarPlugin;
let subscribe: ReturnType<typeof colorByChain>;
let divEl: HTMLDivElement;
async function render() {
Expand All @@ -31,7 +37,7 @@
bgColor,
subscribeEvents: false,
selectInteraction: true,
alphafoldView: true,
alphafoldView: false,
reactive: true,
sequencePanel: true,
hideControls,
Expand All @@ -47,14 +53,34 @@
}
}
function colorByChain(chainColors: ChainColors) {
let allColors: QueryParam[] = [];
for (const [chainId, rgbPerResidue] of Object.entries(chainColors)) {
const colors = colorResidues({
struct_asym_id: chainId,
colors: rgbPerResidue,
});
// add to all colors
allColors = [...allColors, ...colors];
}
return m.events.loadComplete.subscribe(() => {
m.visual.select({ data: allColors });
console.log("color");
});
}
onDestroy(() => {
loseWebGLContext(divEl.querySelector("canvas")!);
m.plugin.dispose();
subscribe.unsubscribe();
});
$: {
if (url && divEl) {
render();
if (chainColors) {
subscribe = colorByChain(chainColors);
}
}
}
</script>
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/lib/openapi/services/DefaultService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,49 @@ export class DefaultService {
},
});
}
/**
* Search Venome Similar Compare
* @param proteinName
* @param proteinCompare
* @returns SimilarProtein Successful Response
* @throws ApiError
*/
public static searchVenomeSimilarCompare(
proteinName: string,
proteinCompare: string,
): CancelablePromise<SimilarProtein> {
return __request(OpenAPI, {
method: 'GET',
url: '/search/venome/similar/{protein_name}/{protein_compare}',
path: {
'protein_name': proteinName,
'protein_compare': proteinCompare,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Plddt Given Protein
* @param proteinName
* @returns number Successful Response
* @throws ApiError
*/
public static getPLddtGivenProtein(
proteinName: string,
): CancelablePromise<Record<string, Array<number>>> {
return __request(OpenAPI, {
method: 'GET',
url: '/protein/pLDDT/{protein_name}',
path: {
'protein_name': proteinName,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Pdb File
* @param proteinName
Expand Down
66 changes: 66 additions & 0 deletions frontend/src/lib/venomeMolstarUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { BACKEND_URL } from "./backend";
import { PDBeMolstarPlugin } from "../../venome-molstar/lib";
import type { InitParams } from "../../venome-molstar/lib/spec";
import type { QueryParam } from "../../venome-molstar/lib/helpers";
import * as d3 from "d3";

export type ResidueColor = { r: number; g: number; b: number };
export type ChainId = string;
export type ChainColors = { [chainId: ChainId]: ResidueColor[] };
export type ChainpLDDT = { [chainId: ChainId]: number[] };

export async function screenshotMolstar(initParams: Partial<InitParams>) {
const { div, molstar } = await renderHeadless(initParams);
Expand Down Expand Up @@ -74,3 +81,62 @@ export const defaultInitParams = (name: string): Partial<InitParams> => ({
"controlInfo",
],
});

export function colorResidues({
colors = [],
entity_id = undefined,
struct_asym_id = "A",
}: {
colors?: { r: number; g: number; b: number }[];
entity_id?: string;
struct_asym_id?: string;
} = {}): QueryParam[] {
let selections: QueryParam[] = [];
for (let i = 0; i < colors.length; i++) {
const color = colors[i];
const residueIndex = i + 1;
const residueColoring = {
entity_id,
struct_asym_id,
color,
start_residue_number: residueIndex,
end_residue_number: residueIndex,
};
selections.push(residueColoring);
}
return selections;
}

export const alphafoldThresholds = ["> 90", "> 70", "> 50", "< 50"];
export const alphafoldColorscheme = [
"rgb(1,83,214)", // > 90
"rgb(100,203,243)", // > 70
"rgb(255,219,18)", // > 50
"rgb(255,125,69)", // < 50
];
export function pLDDTToAlphaFoldResidueColors(pLDDT: number[]): ResidueColor[] {
const colors = pLDDT.map((d) => {
if (d > 90) {
return alphafoldColorscheme[0];
} else if (d > 70) {
return alphafoldColorscheme[1];
} else if (d > 50) {
return alphafoldColorscheme[2];
} else {
return alphafoldColorscheme[3];
}
});
return colors.map((c) => {
const rgb = d3.color(c)!.rgb()!;
return { r: rgb.r, g: rgb.g, b: rgb.b };
});
}

export function pLDDTToResidueColors(pLDDT: number[]): ResidueColor[] {
const interpolate = d3.interpolateSpectral;
const colors = pLDDT.map((d) => interpolate(d / 100));
return colors.map((c) => {
const rgb = d3.color(c)!.rgb()!;
return { r: rgb.r, g: rgb.g, b: rgb.b };
});
}
Loading

0 comments on commit b9acc0a

Please sign in to comment.