diff --git a/backend/src/auth.rs b/backend/src/auth.rs index 21af7da..ce2efbd 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -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}; @@ -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, diff --git a/backend/src/db/mod.rs b/backend/src/db/mod.rs index 892fedf..74a5068 100644 --- a/backend/src/db/mod.rs +++ b/backend/src/db/mod.rs @@ -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}; @@ -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 { let database_url = format!( "postgres://{}:{}@{}:{}/{}", diff --git a/backend/src/db/models.rs b/backend/src/db/models.rs index b12b15b..ae5073e 100644 --- a/backend/src/db/models.rs +++ b/backend/src/db/models.rs @@ -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, @@ -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, diff --git a/backend/src/db/queries.rs b/backend/src/db/queries.rs index 7eb12c1..e1b5927 100644 --- a/backend/src/db/queries.rs +++ b/backend/src/db/queries.rs @@ -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, diff --git a/backend/src/env.rs b/backend/src/env.rs index 0929009..090f291 100644 --- a/backend/src/env.rs +++ b/backend/src/env.rs @@ -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; diff --git a/backend/src/main.rs b/backend/src/main.rs index 479a2ef..c7eb428 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -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::*; diff --git a/backend/src/pathutils.rs b/backend/src/pathutils.rs index a8c0264..c73f31b 100644 --- a/backend/src/pathutils.rs +++ b/backend/src/pathutils.rs @@ -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; @@ -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(), @@ -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, @@ -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 { + // 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))?, diff --git a/backend/src/qp.rs b/backend/src/qp.rs index 42769b3..1ef5ebc 100644 --- a/backend/src/qp.rs +++ b/backend/src/qp.rs @@ -1,3 +1,5 @@ +//! Utils for parsing question paper details + use color_eyre::eyre::eyre; use duplicate::duplicate_item; use serde::Serialize; @@ -5,9 +7,19 @@ 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, } @@ -38,10 +50,21 @@ impl From 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), + /// 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, } @@ -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, @@ -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, @@ -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 { Ok(Self { filelink: env_vars.paths.get_url_from_slug(&self.filelink)?, diff --git a/backend/src/routing/handlers.rs b/backend/src/routing/handlers.rs index f456771..d7e9686 100644 --- a/backend/src/routing/handlers.rs +++ b/backend/src/routing/handlers.rs @@ -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}, @@ -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 = Result<(StatusCode, BackendResponse), AppError>; /// Healthcheck route. Returns a `Hello World.` message if healthy. @@ -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, Query(params): Query>, @@ -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, Json(body): Json, @@ -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, @@ -124,6 +141,7 @@ pub async fn profile(Extension(auth): Extension) -> HandlerReturn, @@ -133,9 +151,12 @@ pub struct EditReq { pub exam: Option, pub approve_status: Option, } + /// 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, State(state): State, @@ -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, @@ -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, mut multipart: Multipart, @@ -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, Json(body): Json, @@ -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, Query(body): Query>, @@ -381,7 +419,7 @@ pub async fn similar( body.get("year") .map(|year| year.parse::()) .transpose()?, - body.get("course_code"), + body.get("course_name"), body.get("semester"), body.get("exam"), ) diff --git a/backend/src/routing/middleware.rs b/backend/src/routing/middleware.rs index 2ab1db6..935ea8a 100644 --- a/backend/src/routing/middleware.rs +++ b/backend/src/routing/middleware.rs @@ -1,3 +1,5 @@ +//! Middleware for the axum router + use axum::{ extract::{Request, State}, middleware::Next, @@ -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, headers: HeaderMap, diff --git a/backend/src/routing/mod.rs b/backend/src/routing/mod.rs index c3f132a..3902d26 100644 --- a/backend/src/routing/mod.rs +++ b/backend/src/routing/mod.rs @@ -1,3 +1,5 @@ +//! Router, [`handlers`], [`middleware`], state, and response utils. + use axum::{ extract::{DefaultBodyLimit, Json}, http::StatusCode, @@ -69,12 +71,14 @@ pub fn get_router(env_vars: &EnvVars, db: Database) -> axum::Router { } #[derive(Clone)] +/// The state of the axum router, containing the environment variables and the database connection. struct RouterState { pub db: db::Database, pub env_vars: EnvVars, } #[derive(Clone, Copy)] +/// The status of a server response enum Status { Success, Error, @@ -101,12 +105,16 @@ impl Serialize for Status { /// Standard backend response format (serialized as JSON) #[derive(serde::Serialize)] struct BackendResponse { + /// Whether the operation succeeded or failed pub status: Status, + /// A message describing the state of the operation (success/failure message) pub message: String, + /// Any optional data sent (only sent if the operation was a success) pub data: Option, } impl BackendResponse { + /// Creates a new success backend response with the given message and data pub fn ok(message: String, data: T) -> (StatusCode, Self) { ( StatusCode::OK, @@ -118,6 +126,7 @@ impl BackendResponse { ) } + /// Creates a new error backend response with the given message, data, and an HTTP status code pub fn error(message: String, status_code: StatusCode) -> (StatusCode, Self) { ( status_code, @@ -136,6 +145,7 @@ impl IntoResponse for BackendResponse { } } +/// A struct representing the error returned by a handler. This is automatically serialized into JSON and sent as an internal server error (500) backend response. The `?` operator can be used anywhere inside a handler to do so. pub(super) struct AppError(color_eyre::eyre::Error); impl IntoResponse for AppError { fn into_response(self) -> axum::response::Response {