Skip to content

Commit

Permalink
docs: tried my best to add some documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
harshkhandeparkar committed Nov 2, 2024
1 parent e12fcc9 commit b25921b
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 4 deletions.
5 changes: 5 additions & 0 deletions backend/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//! Utils for Github OAuth integration and JWT authentication
//!
//! Currently this is only used in the admin dashboard and uses Github OAuth for authentication
use std::collections::BTreeMap;

use color_eyre::eyre::{eyre, Context, ContextCompat};
Expand All @@ -8,6 +12,7 @@ use serde::Deserialize;
use crate::env::EnvVars;

#[derive(Clone)]
/// Struct containing the auth information of a user
pub struct Auth {
pub jwt: String,
pub username: String,
Expand Down
5 changes: 5 additions & 0 deletions backend/src/db/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Database stuff. See submodules also.
use color_eyre::eyre::eyre;
use models::DBAdminDashboardQP;
use sqlx::{postgres::PgPoolOptions, prelude::FromRow, PgPool, Postgres, Transaction};
Expand All @@ -14,16 +16,19 @@ mod models;
mod queries;

#[derive(Clone)]
/// The database
pub struct Database {
connection: PgPool,
}

#[derive(FromRow)]
/// Needed this to use the `query_as()` function of sqlx. There is probably a better way to do this but this is my first time, sorry.
struct Breh {
id: i32,
}

impl Database {
/// Creates a new database connection given the environment variables.
pub async fn new(env_vars: &EnvVars) -> Result<Self, sqlx::Error> {
let database_url = format!(
"postgres://{}:{}@{}:{}/{}",
Expand Down
8 changes: 8 additions & 0 deletions backend/src/db/models.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
//! Database models.
//!
//! These can be (should be) converted to the structs in [`crate::qp`] for sending as a response since the struct also parses the `semester` and `exam` fields and also generates the full static files URL.
//!
//! Use the [`From`] trait implementations.
use crate::qp::Semester;

use super::qp;
use sqlx::{prelude::FromRow, types::chrono};

#[derive(FromRow, Clone)]
/// The fields of a question paper sent to the search endpoint
pub struct DBSearchQP {
id: i32,
filelink: String,
Expand All @@ -16,6 +23,7 @@ pub struct DBSearchQP {
}

#[derive(FromRow, Clone)]
/// The fields of a question paper sent to the admin dashboard endpoint
pub struct DBAdminDashboardQP {
id: i32,
filelink: String,
Expand Down
6 changes: 5 additions & 1 deletion backend/src/db/queries.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
/// Query to get similar papers. Matches `course_code` ($1) always and other optional parameters
//! SQL queries for the database.
//!
//! Some of these are functions that return a query that is dynamically generated based on requirements.
/// Query to get similar papers. Matches `course_code` ($1) always. Other parameters are optional and can be enabled or disabled using the arguments to this function.
pub fn get_similar_papers_query(
year: bool,
course_name: bool,
Expand Down
4 changes: 4 additions & 0 deletions backend/src/env.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//! ### Environment Variables
//!
//! Each field in the struct `EnvVars` corresponds to an environment variable. The environment variable name will be in all capitals. The default values are set using the `arg()` macro of the `clap` crate. Check the source code for the defaults.
use std::path::PathBuf;

use clap::Parser;
Expand Down
4 changes: 4 additions & 0 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//! ### IQPS Backend
//!
//! The backend is divided into multiple modules. The [`routing`] module contains all the route handlers and the [`db`] module contains all database queries and models. Other modules are utilities used throughout the backend.
use clap::Parser;
use tracing_subscriber::prelude::*;

Expand Down
21 changes: 21 additions & 0 deletions backend/src/pathutils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//! Utils for parsing paths on the server and to store/retrieve paths from the database
//! A "slug" is the part of the path common to the question paper and is stored in the database. Depending on the requirements, either a URL (eg: static.metakgp.org) or a path (/srv/static) can be prepended to the slug to get the final path to copy/serve/move the question paper to/from.
use std::{fs, path::{self, Path, PathBuf}};

use color_eyre::eyre::eyre;
Expand All @@ -6,20 +9,27 @@ use url::Url;
/// A category of papers, can also be used to represent the directory where these papers are stored
#[allow(unused)]
pub enum PaperCategory {
/// Unapproved paper
Unapproved,
/// Approved paper
Approved,
/// Library paper (scraped using the peqp scraper)
Library,
}

#[derive(Clone, Default)]
/// A set of paths (absolute, relative, or even URLs) for all three categories of papers (directories)
struct PathTriad {
/// Unapproved paper path
pub unapproved: PathBuf,
/// Approved paper path
pub approved: PathBuf,
/// Library paper path
pub library: PathBuf,
}

impl PathTriad {
/// Gets the path in the triad corresponding to the given paper category.
pub fn get(&self, category: PaperCategory) -> PathBuf {
match category {
PaperCategory::Approved => self.approved.to_owned(),
Expand All @@ -31,6 +41,7 @@ impl PathTriad {

#[derive(Clone)]
#[allow(unused)]
/// Struct containing all the paths and URLs required to parse or create any question paper's slug, absolute path, or URL.
pub struct Paths {
/// URL of the static files server
static_files_url: Url,
Expand Down Expand Up @@ -60,18 +71,28 @@ impl Default for Paths {

#[allow(unused)]
impl Paths {
/// Creates a new `Paths` struct
/// # Arguments
///
/// * `static_files_url` - The static files server URL (eg: https://static.metakgp.org)
/// * `static_file_storage_location` - The path to the location on the server from which the static files are served (eg: /srv/static)
/// * `uploaded_qps_relative_path` - The path to the uploaded question papers, relative to the static files storage location. (eg: /iqps/uploaded)
/// * `library_qps_relative_path` - The path to the library question papers, relative to the static files storage location. (eg: /peqp/qp)
pub fn new(
static_files_url: &str,
static_file_storage_location: &Path,
uploaded_qps_relative_path: &Path,
library_qps_relative_path: &Path,
) -> Result<Self, color_eyre::eyre::Error> {
// The slugs for each of the uploaded papers directories
let path_slugs = PathTriad {
// Use subdirectories `/unapproved` and `/approved` inside the uploaded qps path
unapproved: uploaded_qps_relative_path.join("unapproved"),
approved: uploaded_qps_relative_path.join("approved"),
library: library_qps_relative_path.to_owned(),
};

// The absolute system paths for each of the directories
let system_paths = PathTriad {
unapproved: path::absolute(static_file_storage_location.join(&path_slugs.unapproved))?,
approved: path::absolute(static_file_storage_location.join(&path_slugs.approved))?,
Expand Down
28 changes: 28 additions & 0 deletions backend/src/qp.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
//! Utils for parsing question paper details
use color_eyre::eyre::eyre;
use duplicate::duplicate_item;
use serde::Serialize;

use crate::env::EnvVars;

#[derive(Clone, Copy)]
/// Represents a semester.
///
/// It can be parsed from a [`String`] using the `.try_from()` function. An error will be returned if the given string has an invalid value.
///
/// This value can be converted back into a [`String`] using the [`From`] trait implementation.
pub enum Semester {
/// Autumn semester, parsed from `autumn`
Autumn,
/// Spring semester, parsed from `spring`
Spring,
/// Unknown/wildcard semester, parsed from an empty string.
///
/// Note that this is different from an invalid value and is used to represent papers for which the semester is not known. An invalid value would be `puppy` or `Hippopotomonstrosesquippedaliophobia` for example.
Unknown,
}

Expand Down Expand Up @@ -38,10 +50,21 @@ impl From<Semester> for String {
}

#[derive(Clone, Copy)]
/// Represents the exam type of the paper.
///
/// Can be converted to and parsed from a String using the [`From`] and [`TryFrom`] trait implementations.
pub enum Exam {
/// Mid-semester examination, parsed from `midsem`
Midsem,
/// End-semester examination, parsed from `endsem`
Endsem,
/// Class test, parsed from either `ct` or `ct` followed by a number (eg: `ct1` or `ct10`).
///
/// The optional number represents the number of the class test (eg: class test 1 or class test 21). This will be None if the number is not known, parsed from `ct`.
CT(Option<usize>),
/// Unknown class test, parsed from an empty string.
///
/// Note that this is different from an invalid value and is used to represent papers for which the exam is not known. An invalid value would be `catto` or `metakgp` for example.
Unknown,
}

Expand Down Expand Up @@ -98,6 +121,7 @@ impl Serialize for ExamSem {
}

#[derive(Serialize, Clone)]
/// The fields of a question paper sent from the search endpoint
pub struct SearchQP {
pub id: i32,
pub filelink: String,
Expand All @@ -110,6 +134,9 @@ pub struct SearchQP {
}

#[derive(Serialize, Clone)]
/// The fields of a question paper sent from the admin dashboard endpoints.
///
/// This includes fields such as `approve_status` and `upload_timestamp` that would only be relevant to the dashboard.
pub struct AdminDashboardQP {
pub id: i32,
pub filelink: String,
Expand All @@ -129,6 +156,7 @@ pub struct AdminDashboardQP {
[ AdminDashboardQP ];
)]
impl QP {
/// Returns the question paper with the full static files URL in the `filelink` field instead of just the slug. See the [`crate::pathutils`] module for what a slug is.
pub fn with_url(self, env_vars: &EnvVars) -> Result<Self, color_eyre::eyre::Error> {
Ok(Self {
filelink: env_vars.paths.get_url_from_slug(&self.filelink)?,
Expand Down
40 changes: 39 additions & 1 deletion backend/src/routing/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
//! All endpoint handlers and their response types.
//!
//! All endpoints accept JSON or URL query parameters as the request. The response of each handler is a [`BackendResponse`] serialized as JSON and the return type of the handler function determines the schema of the data sent in the response (if successful)
//!
//! The request format is described
use axum::{
body::Bytes,
extract::{Json, Multipart},
Expand All @@ -22,6 +28,7 @@ use crate::{

use super::{AppError, BackendResponse, RouterState, Status};

/// The return type of a handler function. T is the data type returned if the operation was a success
type HandlerReturn<T> = Result<(StatusCode, BackendResponse<T>), AppError>;

/// Healthcheck route. Returns a `Hello World.` message if healthy.
Expand All @@ -47,6 +54,10 @@ pub async fn get_unapproved(
}

/// Searches for question papers given a query and an optional `exam` parameter.
///
/// # Request Query Parameters
/// * `query`: The query string to search in the question papers (searches course name or code)
/// * `exam` (optional): A filter for the question paper by the exam field.
pub async fn search(
State(state): State<RouterState>,
Query(params): Query<HashMap<String, String>>,
Expand Down Expand Up @@ -80,15 +91,20 @@ pub async fn search(
}

#[derive(Deserialize)]
/// The request format for the OAuth endpoint
pub struct OAuthReq {
code: String,
}

#[derive(Serialize)]
/// The response format for the OAuth endpoint
pub struct OAuthRes {
token: String,
}

/// Takes a Github OAuth code and returns a JWT auth token to log in a user if authorized
///
/// Request format - [`OAuthReq`]
pub async fn oauth(
State(state): State<RouterState>,
Json(body): Json<OAuthReq>,
Expand All @@ -107,6 +123,7 @@ pub async fn oauth(
}

#[derive(Serialize)]
/// The response format for the user profile endpoint
pub struct ProfileRes {
token: String,
username: String,
Expand All @@ -124,6 +141,7 @@ pub async fn profile(Extension(auth): Extension<Auth>) -> HandlerReturn<ProfileR
}

#[derive(Deserialize)]
/// The request format for the paper edit endpoint
pub struct EditReq {
pub id: i32,
pub course_code: Option<String>,
Expand All @@ -133,9 +151,12 @@ pub struct EditReq {
pub exam: Option<String>,
pub approve_status: Option<bool>,
}

/// Paper edit endpoint (for admin dashboard)
/// Takes a JSON request body. The `id` field is required.
/// Other optional fields can be set to change that particular value in the paper.
///
/// Request format - [`EditReq`]
pub async fn edit(
Extension(auth): Extension<Auth>,
State(state): State<RouterState>,
Expand Down Expand Up @@ -169,6 +190,7 @@ pub async fn edit(
}

#[derive(Deserialize)]
/// The details for an uploaded question paper file
pub struct FileDetails {
pub course_code: String,
pub course_name: String,
Expand All @@ -181,13 +203,19 @@ pub struct FileDetails {
/// 10 MiB file size limit
const FILE_SIZE_LIMIT: usize = 10 << 20;
#[derive(Serialize)]
/// The status of an uploaded question paper file
pub struct UploadStatus {
/// The filename
filename: String,
/// Whether the file was successfully uploaded
status: Status,
/// A message describing the status
message: String,
}

/// Uploads question papers to the server
///
/// Request format - Multipart form with a `file_details` field of the format [`FileDetails`]
pub async fn upload(
State(state): State<RouterState>,
mut multipart: Multipart,
Expand Down Expand Up @@ -337,11 +365,14 @@ pub async fn upload(
}

#[derive(Deserialize)]
/// The request format for the delete endpoint
pub struct DeleteReq {
id: i32,
}

/// Deletes a given paper. Library papers cannot be deleted.
///
/// Request format - [`DeleteReq`]
pub async fn delete(
State(state): State<RouterState>,
Json(body): Json<DeleteReq>,
Expand All @@ -362,6 +393,13 @@ pub async fn delete(
}

/// Fetches all question papers that match one or more properties specified. `course_name` is compulsory.
///
/// # Request Query Parameters
/// * `course_code`: The course code of the question paper. (required)
/// * `year` (optional): The year of the question paper.
/// * `course_name` (optional): The course name (exact).
/// * `semester` (optional): The semester (autumn/spring)
/// * `exam` (optional): The exam field (midsem/endsem/ct)
pub async fn similar(
State(state): State<RouterState>,
Query(body): Query<HashMap<String, String>>,
Expand All @@ -381,7 +419,7 @@ pub async fn similar(
body.get("year")
.map(|year| year.parse::<i32>())
.transpose()?,
body.get("course_code"),
body.get("course_name"),
body.get("semester"),
body.get("exam"),
)
Expand Down
4 changes: 2 additions & 2 deletions backend/src/routing/middleware.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Middleware for the axum router
use axum::{
extract::{Request, State},
middleware::Next,
Expand All @@ -10,8 +12,6 @@ use crate::auth;
use super::{AppError, BackendResponse, RouterState};

/// Verifies the JWT and authenticates a user. If the JWT is invalid, the user is sent an unauthorized status code. If the JWT is valid, the authentication is added to the state.
///
/// TODO: THIS IS DUM DUM, CHANGE IT, ADD THE STATE TO THE REQUEST, A SHARED AUTH MUTEX MAKES NO SENSE
pub async fn verify_jwt_middleware(
State(state): State<RouterState>,
headers: HeaderMap,
Expand Down
Loading

0 comments on commit b25921b

Please sign in to comment.