Merge branch 'sections-backend' into develop
This commit is contained in:
commit
1441e3e601
21 changed files with 1029 additions and 388 deletions
|
|
@ -1,5 +1,11 @@
|
|||
//! The db module contains all Diesel-related logic. This is to prevent the various Diesel imports
|
||||
//! from poluting other modules' namespaces.
|
||||
|
||||
pub mod posts;
|
||||
pub mod sections;
|
||||
pub mod tokens;
|
||||
pub mod users;
|
||||
|
||||
pub use sections::{NewSection, Section};
|
||||
pub use tokens::{NewRefreshToken, RefreshToken};
|
||||
pub use users::{NewUser, User};
|
||||
|
|
|
|||
58
src/db/posts.rs
Normal file
58
src/db/posts.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use chrono::NaiveDate;
|
||||
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
errors::{RbError, RbResult},
|
||||
schema::{posts, posts::dsl::*},
|
||||
};
|
||||
|
||||
#[derive(Queryable)]
|
||||
pub struct Post
|
||||
{
|
||||
pub id: Uuid,
|
||||
pub section_id: Uuid,
|
||||
pub title: Option<String>,
|
||||
pub publish_date: NaiveDate,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "posts"]
|
||||
pub struct NewPost
|
||||
{
|
||||
pub section_id: Uuid,
|
||||
pub title: Option<String>,
|
||||
pub publish_date: NaiveDate,
|
||||
}
|
||||
|
||||
/// Returns all posts in the database; should be used with care as this method could quickly return
|
||||
/// a large amount of data.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - a reference to a database connection
|
||||
pub fn all(conn: &PgConnection) -> RbResult<Vec<Post>>
|
||||
{
|
||||
posts
|
||||
.load::<Post>(conn)
|
||||
.map_err(|_| RbError::DbError("Couldn't get all posts."))
|
||||
}
|
||||
|
||||
/// Insert a new post into the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - reference to a database connection
|
||||
/// * `new_post` - the new post object to insert
|
||||
pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<()>
|
||||
{
|
||||
insert_into(posts)
|
||||
.values(new_post)
|
||||
.execute(conn)
|
||||
.map_err(|_| RbError::DbError("Couldn't insert post."))?;
|
||||
|
||||
// TODO check for conflict?
|
||||
|
||||
Ok(())
|
||||
}
|
||||
63
src/db/sections.rs
Normal file
63
src/db/sections.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
//! Handles all section-related database operations.
|
||||
|
||||
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
errors::{RbError, RbResult},
|
||||
schema::{sections, sections::dsl::*},
|
||||
};
|
||||
|
||||
/// Represents a section contained in the database.
|
||||
#[derive(Queryable)]
|
||||
pub struct Section
|
||||
{
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub is_default: bool,
|
||||
pub has_titles: bool,
|
||||
}
|
||||
|
||||
/// A new section to be added into the database.
|
||||
#[derive(Deserialize, Insertable)]
|
||||
#[table_name = "sections"]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NewSection
|
||||
{
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
is_default: Option<bool>,
|
||||
has_titles: Option<bool>,
|
||||
}
|
||||
|
||||
/// Returns all sections in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - reference to a database connection
|
||||
pub fn all(conn: &PgConnection) -> RbResult<Vec<Section>>
|
||||
{
|
||||
sections
|
||||
.load::<Section>(conn)
|
||||
.map_err(|_| RbError::DbError("Couldn't get all sections"))
|
||||
}
|
||||
|
||||
/// Inserts a new section into the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - reference to a database connection
|
||||
/// * `new_section` - the new section to be added
|
||||
pub fn create(conn: &PgConnection, new_section: &NewSection) -> RbResult<()>
|
||||
{
|
||||
insert_into(sections)
|
||||
.values(new_section)
|
||||
.execute(conn)
|
||||
.map_err(|_| RbError::DbError("Couldn't insert section."))?;
|
||||
|
||||
// TODO check for conflict?
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
//! Handles refresh token-related database operations.
|
||||
|
||||
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -6,6 +8,7 @@ use crate::{
|
|||
schema::{refresh_tokens, refresh_tokens::dsl::*},
|
||||
};
|
||||
|
||||
/// A refresh token as stored in the database
|
||||
#[derive(Queryable)]
|
||||
pub struct RefreshToken
|
||||
{
|
||||
|
|
@ -15,6 +18,7 @@ pub struct RefreshToken
|
|||
pub last_used_at: Option<chrono::NaiveDateTime>,
|
||||
}
|
||||
|
||||
/// A new refresh token to be added into the database
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "refresh_tokens"]
|
||||
pub struct NewRefreshToken
|
||||
|
|
@ -24,6 +28,12 @@ pub struct NewRefreshToken
|
|||
pub expires_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
// TODO add pagination as this could grow very quickly
|
||||
/// Returns all refresh tokens contained in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
pub fn all(conn: &PgConnection) -> RbResult<Vec<RefreshToken>>
|
||||
{
|
||||
refresh_tokens
|
||||
|
|
@ -31,18 +41,30 @@ pub fn all(conn: &PgConnection) -> RbResult<Vec<RefreshToken>>
|
|||
.map_err(|_| RbError::DbError("Couldn't get all refresh tokens."))
|
||||
}
|
||||
|
||||
/// Insert a new refresh token into the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
/// * `new_refresh_token` - token to insert
|
||||
pub fn create(conn: &PgConnection, new_refresh_token: &NewRefreshToken) -> RbResult<()>
|
||||
{
|
||||
insert_into(refresh_tokens)
|
||||
.values(new_refresh_token)
|
||||
.execute(conn)
|
||||
.map_err(|_| RbError::Custom("Couldn't insert refresh token."))?;
|
||||
.map_err(|_| RbError::DbError("Couldn't insert refresh token."))?;
|
||||
|
||||
// TODO check for conflict?
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the token & user data associated with the given refresh token value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
/// * `token_val` - token value to search for
|
||||
pub fn find_with_user(
|
||||
conn: &PgConnection,
|
||||
token_val: &[u8],
|
||||
|
|
@ -53,10 +75,20 @@ pub fn find_with_user(
|
|||
.inner_join(crate::schema::users::dsl::users)
|
||||
.filter(token.eq(token_val))
|
||||
.first::<(RefreshToken, super::users::User)>(conn)
|
||||
.map_err(|_| RbError::Custom("Couldn't get refresh token & user."))
|
||||
.map_err(|_| RbError::DbError("Couldn't get refresh token & user."))
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Updates a token's `last_used_at` column value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
/// * `token_` - value of the refresh token to update
|
||||
/// * `last_used_at_` - date value to update column with
|
||||
///
|
||||
/// **NOTE**: argument names use trailing underscores as to not conflict with Diesel's imported dsl
|
||||
/// names.
|
||||
pub fn update_last_used_at(
|
||||
conn: &PgConnection,
|
||||
token_: &[u8],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
//! Handles user-related database operations.
|
||||
|
||||
use diesel::{prelude::*, AsChangeset, Insertable, Queryable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
|
@ -7,6 +9,7 @@ use crate::{
|
|||
schema::{users, users::dsl::*},
|
||||
};
|
||||
|
||||
/// A user as stored in the database.
|
||||
#[derive(Queryable, Serialize)]
|
||||
pub struct User
|
||||
{
|
||||
|
|
@ -18,6 +21,7 @@ pub struct User
|
|||
pub admin: bool,
|
||||
}
|
||||
|
||||
/// A new user to add to the database.
|
||||
#[derive(Insertable, AsChangeset, Deserialize)]
|
||||
#[table_name = "users"]
|
||||
pub struct NewUser
|
||||
|
|
@ -27,6 +31,11 @@ pub struct NewUser
|
|||
pub admin: bool,
|
||||
}
|
||||
|
||||
/// Returns all users in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
pub fn all(conn: &PgConnection) -> RbResult<Vec<User>>
|
||||
{
|
||||
users
|
||||
|
|
@ -34,11 +43,23 @@ pub fn all(conn: &PgConnection) -> RbResult<Vec<User>>
|
|||
.map_err(|_| RbError::DbError("Couldn't get all users."))
|
||||
}
|
||||
|
||||
/// Find a user with a given ID.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
/// * `user_id` - ID to search for
|
||||
pub fn find(conn: &PgConnection, user_id: Uuid) -> Option<User>
|
||||
{
|
||||
users.find(user_id).first::<User>(conn).ok()
|
||||
}
|
||||
|
||||
/// Find a user with a given username.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
/// * `username_` - username to search for
|
||||
pub fn find_by_username(conn: &PgConnection, username_: &str) -> RbResult<User>
|
||||
{
|
||||
Ok(users
|
||||
|
|
@ -47,6 +68,12 @@ pub fn find_by_username(conn: &PgConnection, username_: &str) -> RbResult<User>
|
|||
.map_err(|_| RbError::DbError("Couldn't find users by username."))?)
|
||||
}
|
||||
|
||||
/// Insert a new user into the database
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
/// * `new_user` - user to insert
|
||||
pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
||||
{
|
||||
let count = diesel::insert_into(users)
|
||||
|
|
@ -61,6 +88,12 @@ pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Either create a new user or update an existing one on conflict.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `conn` - database connection to use
|
||||
/// * `new_user` - user to insert/update
|
||||
pub fn create_or_update(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
||||
{
|
||||
diesel::insert_into(users)
|
||||
|
|
@ -74,6 +107,12 @@ pub fn create_or_update(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete the user with the given ID.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// `conn` - database connection to use
|
||||
/// `user_id` - ID of user to delete
|
||||
pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
|
||||
{
|
||||
diesel::delete(users.filter(id.eq(user_id)))
|
||||
|
|
@ -83,6 +122,14 @@ pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Block a user given an ID.
|
||||
/// In practice, this means updating the user's entry so that the `blocked` column is set to
|
||||
/// `true`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// `conn` - database connection to use
|
||||
/// `user_id` - ID of user to block
|
||||
pub fn block(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
|
||||
{
|
||||
diesel::update(users.filter(id.eq(user_id)))
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ impl RbError
|
|||
RbError::AuthInvalidRefreshToken => "This refresh token is not valid.",
|
||||
RbError::AuthDuplicateRefreshToken => {
|
||||
"This refresh token has already been used. The user has been blocked."
|
||||
}
|
||||
},
|
||||
RbError::AuthMissingHeader => "Missing Authorization header.",
|
||||
|
||||
RbError::UMDuplicateUser => "This user already exists.",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use sha2::Sha256;
|
|||
|
||||
use crate::{auth::jwt::Claims, errors::RbError, RbConfig};
|
||||
|
||||
/// Extracts a "Authorization: Bearer" string from the headers.
|
||||
/// Extracts an "Authorization: Bearer" string from the headers.
|
||||
pub struct Bearer<'a>(&'a str);
|
||||
|
||||
#[rocket::async_trait]
|
||||
|
|
@ -22,7 +22,7 @@ impl<'r> FromRequest<'r> for Bearer<'r>
|
|||
{
|
||||
// If the header isn't present, just forward to the next route
|
||||
let header = match req.headers().get_one("Authorization") {
|
||||
None => return Outcome::Failure((Status::BadRequest, Self::Error::AuthMissingHeader)),
|
||||
None => return Outcome::Forward(()),
|
||||
Some(val) => val,
|
||||
};
|
||||
|
||||
|
|
@ -31,12 +31,10 @@ impl<'r> FromRequest<'r> for Bearer<'r>
|
|||
}
|
||||
|
||||
// Extract the jwt token from the header
|
||||
let auth_string = match header.get(7..) {
|
||||
Some(s) => s,
|
||||
None => return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)),
|
||||
};
|
||||
|
||||
Outcome::Success(Self(auth_string))
|
||||
match header.get(7..) {
|
||||
Some(s) => Outcome::Success(Self(s)),
|
||||
None => Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,14 +61,14 @@ impl<'r> FromRequest<'r> for Jwt
|
|||
Status::InternalServerError,
|
||||
Self::Error::Custom("Failed to do Hmac thing."),
|
||||
))
|
||||
}
|
||||
},
|
||||
};
|
||||
// Verify token using key
|
||||
let claims: Claims = match bearer.verify_with_key(&key) {
|
||||
Ok(claims) => claims,
|
||||
Err(_) => {
|
||||
return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Outcome::Success(Self(claims))
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ pub mod db;
|
|||
pub mod errors;
|
||||
pub mod guards;
|
||||
pub(crate) mod schema;
|
||||
pub mod sections;
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
|
@ -110,4 +111,5 @@ fn rocket() -> _
|
|||
"/api/admin",
|
||||
routes![admin::get_users, admin::create_user, admin::get_user_info],
|
||||
)
|
||||
.mount("/api/sections", routes![sections::create_section])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
table! {
|
||||
posts (id) {
|
||||
id -> Uuid,
|
||||
section_id -> Uuid,
|
||||
title -> Nullable<Varchar>,
|
||||
publish_date -> Date,
|
||||
content -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
refresh_tokens (token) {
|
||||
token -> Bytea,
|
||||
|
|
@ -7,6 +17,16 @@ table! {
|
|||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
sections (id) {
|
||||
id -> Uuid,
|
||||
title -> Varchar,
|
||||
description -> Nullable<Text>,
|
||||
is_default -> Bool,
|
||||
has_titles -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
users (id) {
|
||||
id -> Uuid,
|
||||
|
|
@ -17,6 +37,7 @@ table! {
|
|||
}
|
||||
}
|
||||
|
||||
joinable!(posts -> sections (section_id));
|
||||
joinable!(refresh_tokens -> users (user_id));
|
||||
|
||||
allow_tables_to_appear_in_same_query!(refresh_tokens, users,);
|
||||
allow_tables_to_appear_in_same_query!(posts, refresh_tokens, sections, users,);
|
||||
|
|
|
|||
24
src/sections.rs
Normal file
24
src/sections.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
//! This module handles management of site sections (aka blogs).
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
use crate::{db, errors::RbResult, guards::Admin, RbDbConn};
|
||||
|
||||
/// Route for creating a new section.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `_admin` - guard ensuring user is admin
|
||||
/// * `conn` - guard providing a connection to the database
|
||||
/// * `new_section` - Json-encoded NewSection object
|
||||
#[post("/", data = "<new_section>")]
|
||||
pub async fn create_section(
|
||||
_admin: Admin,
|
||||
conn: RbDbConn,
|
||||
new_section: Json<db::NewSection>,
|
||||
) -> RbResult<()>
|
||||
{
|
||||
Ok(conn
|
||||
.run(move |c| db::sections::create(c, &new_section.into_inner()))
|
||||
.await?)
|
||||
}
|
||||
Reference in a new issue