diff --git a/client-solid/index.html b/client-solid/index.html index 363220e..35ca88f 100644 --- a/client-solid/index.html +++ b/client-solid/index.html @@ -9,7 +9,7 @@ FrostByteSolid - +
diff --git a/client-solid/src/Login.tsx b/client-solid/src/Login.tsx deleted file mode 100644 index 1af585c..0000000 --- a/client-solid/src/Login.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { createSignal, useContext } from "solid-js"; -import { LoginContext, ModalContext } from "./Root"; - -export function LoginForm() { - const modal_ctx = useContext(ModalContext); - const login_ctx = useContext(LoginContext); - const [username, setUsername] = createSignal(""); - const [password, setPassword] = createSignal(""); - const [waiting, setWaiting] = createSignal(false); - const [error, setError] = createSignal(false); - - async function loginFailed() { - setError(true); - setWaiting(false); - setTimeout(() => { - setError(false); - }, 1000); - } - - return ( -
- - { - setUsername(e.target.value); - }} /> - - { - setPassword(e.target.value); - }} /> - -
- ); -} - -// This function is responsible for sending the login request to the server -// and storing the token in localstorage -export async function submitLogin( - username: string, - password: string -): Promise { - const response = await fetch("/api/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, password }), - }); - - if (response.ok) { - const data = await response.json(); - if (data.token && data.username) { - localStorage.setItem("token", data.token); - localStorage.setItem("username", data.username); - return data.token; - } - } - return ""; -} - diff --git a/client-solid/src/Navbar.tsx b/client-solid/src/Navbar.tsx deleted file mode 100644 index ca5e4b8..0000000 --- a/client-solid/src/Navbar.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useContext } from "solid-js"; -import { A } from "@solidjs/router"; -import { LoginContext } from "./Root"; -import { ModalContext } from "./Root"; -import { LoginForm } from "./Login"; - -function Menu() { - let login_ctx = useContext(LoginContext); - return ( - - ); -} - -export function Navbar() { - let modal_ctx = useContext(ModalContext); - let login_ctx = useContext(LoginContext); - - return ( - - ); -} - -// This is a modal -export function Login() { - const modal_ctx = useContext(ModalContext); - return ( - - - - - ); -} diff --git a/client-solid/src/Posts.tsx b/client-solid/src/Posts.tsx deleted file mode 100644 index 85e8dd1..0000000 --- a/client-solid/src/Posts.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { createSignal } from "solid-js"; -import { getPosts } from "./api"; -import { Post } from "./api"; -import { useNavigate } from "@solidjs/router"; - -export function Posts() { - const [posts, setPosts] = createSignal([] as Post[]); - const [loading, setLoading] = createSignal(true); - - getPosts().then((posts) => { - setPosts(posts as any); - setLoading(false); - }); - - return ( -
- {loading() ? ( - - ) : ( - <> - )} - {posts().map((post) => { - if (post.content == "") return; // Filtering out empty posts, remove this later - return ; - })} -
- ); -} - -// This is the card container for a post -export function PostSegment({ post }: { post: Post }) { - const nav = useNavigate(); - return ( -
nav("/post/" + post?.id)} - class="card bg-base-200 shadow-lg compact text-base-content w-full" - > -
-

{post?.content}

-
-
- ); -} diff --git a/client-solid/src/Primary.tsx b/client-solid/src/Primary.tsx deleted file mode 100644 index 13d0eae..0000000 --- a/client-solid/src/Primary.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Route, Routes } from "@solidjs/router"; -import { Posts } from "./Posts"; -import { SinglePost } from "./SinglePost"; -import { NewPostInputArea } from "./Root"; - -// Primary is the section of the page that holds the main content -export function Primary() { - return ( - - } /> - } /> - } /> - 404} /> - - ); -} diff --git a/client-solid/src/Root.tsx b/client-solid/src/Root.tsx index ef7aaa7..254424a 100644 --- a/client-solid/src/Root.tsx +++ b/client-solid/src/Root.tsx @@ -1,111 +1,143 @@ -import { Accessor, Show, createSignal, useContext } from "solid-js"; +import { createSignal } from "solid-js"; import { createContext } from "solid-js"; -import { createPost } from "./api"; -import { NewPost } from "./api"; -import { Navbar } from "./Navbar"; -import { Primary } from "./Primary"; -import { Login } from "./Navbar"; -import { useNavigate } from "@solidjs/router"; +import { Route, Routes, A } from "@solidjs/router"; -// Representing the state of varoious modals. -// So far we only have one modal, but we can add more later -// by adding more fields to this interface, or maybe an enum -interface ModalContextType { - loginModalOpen: Accessor; - setLoginModalOpen: (value: boolean) => void; -} +import { createPost, getPosts } from "./api"; +import { Post, NewPost } from "./api"; -interface LoginContextType { - token: Accessor; - setToken: (value: string) => void; - username: Accessor; - setUsername: (value: string) => void; -} - -// It is unclear to me if this is the idiomatic way to do this in Solid -export const ModalContext = createContext(); -export const LoginContext = createContext(); +export const TestContext = createContext("Test123"); function Root() { - // All of these are passed into context providers - const [loginModalOpen, setLoginModalOpen] = createSignal(false); - const [token, setToken] = createSignal(""); - const [username, setUsername] = createSignal(""); - - // This may not be the best place to do this. - localStorage.getItem("token") && setToken(localStorage.getItem("token")!); - localStorage.getItem("username") && - setUsername(localStorage.getItem("username")!); - return ( <> - - -
- - -
- -
+ +
+ +
+
- - +
+
); } -export function NewPostInputArea() { +function Navbar() { + return ( + + ); +} + +function Menu() { + return ( + + ); +} + +function NewPostInputArea() { const [content, setContent] = createSignal(""); - const [waiting, setWaiting] = createSignal(false); - const login_ctx = useContext(LoginContext); + return ( +
+ + +
+ ); +} - const nav = useNavigate(); +function Posts() { + const [posts, setPosts] = createSignal([] as Post[]); + const [loading, setLoading] = createSignal(true); - const sendPost = () => { - setWaiting(true); - const response = createPost({ - content: content(), - token: login_ctx?.token(), - } as NewPost); - if (response) { - response.then(() => { - setWaiting(false); - setContent(""); - nav("/"); - }); - } - }; + getPosts().then((posts) => { + setPosts(posts as any); + setLoading(false) + }); return ( - - } - > -
- - +
+ { loading() ? : <> } + {posts().map((post) => { + if (post.content == "") return; // Filtering out empty posts, remove this later + return ; + })} +
+ ); +} + +function PostSegment({ post }: { post: Post }) { + return ( +
+
+

{post.content}

+ {/*

{post.votes.up}

+

{post.votes.down}

*/}
- +
+ ); +} + +function Primary() { + return ( + + } /> + } /> + Boards
} /> + } /> + + ); +} + +function Login() { + return ( +
+ Login + +
); } diff --git a/client-solid/src/SinglePost.tsx b/client-solid/src/SinglePost.tsx deleted file mode 100644 index b4440be..0000000 --- a/client-solid/src/SinglePost.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useParams } from "@solidjs/router"; -import { Show, Suspense, createResource } from "solid-js"; -import { getPost } from "./api"; -import { PostSegment } from "./Posts"; - -export function SinglePost() { - const params = useParams(); - const [post] = createResource(params.postid, getPost); - - return ( - Some loading message
}> -
- - - -
- - ); -} diff --git a/client-solid/src/api.ts b/client-solid/src/api.ts index 6b9660f..b1ed3a6 100644 --- a/client-solid/src/api.ts +++ b/client-solid/src/api.ts @@ -1,4 +1,6 @@ -// This file contains types and functions related to interacting with the API. +// const PORT = 3000; +// const API_URL = `http://localhost:${PORT}/api/`; +// const API_URL2 = new URL(API_URL); export interface NewPost { content: string; @@ -11,12 +13,13 @@ interface Votes { } export interface Post extends NewPost { - id: string; + uuid: string; createdAt: string; votes: Votes; } export async function getPosts(): Promise { +// const res = await fetch(`${API_URL}/posts`); const res = await fetch("/api/posts"); const data = await res.json(); return data; @@ -24,9 +27,7 @@ export async function getPosts(): Promise { export async function getPost(id: string): Promise { const res = await fetch(`/api/posts/${id}`); - console.log(res) const data = await res.json(); - console.log(data) return data; } diff --git a/justfile b/justfile index 1ec67a7..5bce120 100644 --- a/justfile +++ b/justfile @@ -4,15 +4,14 @@ runtime := "podman" dev: start-debug @echo "Cd into client and run 'npm run dev' to start the client in dev mode." -[private] +# Builds the client with npm (result in client/dist npm-install directory: cd {{directory}} && npm install # Builds the client with npm (result in client/dist) [private] -npm-build directory: (npm-install directory) - cd {{directory}} && npm run build - @echo "Built client at {{directory}}/dist" +npm-build: (npm-install "client-solid") + cd client && npm run build # Builds a debug container [private] @@ -32,13 +31,7 @@ build-container-release: # Builds a release container and runs it start-release: build-container-release remove-podman-containers - {{runtime}} run -d -e DATABASE_URL=sqlite:release.db -p 8080:8080 --name frostbyte fb-server - -init-sqlx: - echo "DATABASE_URL=sqlite:debug.db" > server/.env - cd server && sqlx database create - cd server && sqlx migrate run - cd server && cargo sqlx prepare + {{runtime}} run -d -p 8080:8080 --name frostbyte fb-server # Removes and stops any containers related to the project [private] @@ -63,8 +56,6 @@ clean: {{runtime}} image rm -f fb-server-debug rm -rf client/dist rm -rf client/node_modules - rm -rf client-solid/dist - rm -rf client-solid/node_modules rm -rf server/public rm -rf server/target @echo "Cleaned up! Make sure to run 'just nuke' to nuke everything podman related." diff --git a/server/Cargo.lock b/server/Cargo.lock index 3dfcfc7..93d4286 100755 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -391,18 +391,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "biosvg" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51c50785b88aca88dc4417a3aede395dac83b5031286f02523ddf4839c59e7f8" -dependencies = [ - "once_cell", - "rand", - "regex", - "thiserror", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -1792,7 +1780,6 @@ dependencies = [ "actix-files", "actix-web", "argon2", - "biosvg", "chrono", "clap", "dotenvy", diff --git a/server/Cargo.toml b/server/Cargo.toml index f770f6e..8afda9d 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" actix-files = "0.6.2" actix-web = "4.4.0" argon2 = { version = "0.5.2", features = ["zeroize"] } -biosvg = "0.1.3" chrono = { version = "0.4.31", features = ["serde"] } clap = { version = "4.4.5", features = ["derive"] } dotenvy = "0.15.7" diff --git a/server/migrations/0001_users_table.sql b/server/migrations/0001_users_table.sql index b9ccfdb..76976d2 100644 --- a/server/migrations/0001_users_table.sql +++ b/server/migrations/0001_users_table.sql @@ -1,31 +1,9 @@ -CREATE TABLE - IF NOT EXISTS users ( - id INTEGER PRIMARY KEY NOT NULL, - username TEXT NOT NULL UNIQUE, - password TEXT NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP - ); +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL, + password TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); --- Create a trigger to set created_at and updated_at on INSERT -CREATE TRIGGER IF NOT EXISTS set_created_at AFTER INSERT ON users BEGIN -UPDATE users -SET - created_at = CURRENT_TIMESTAMP -WHERE - id = NEW.id; - -END; - --- Create a trigger to set updated_at on UPDATE -CREATE TRIGGER IF NOT EXISTS set_updated_at AFTER -UPDATE ON users BEGIN -UPDATE users -SET - updated_at = CURRENT_TIMESTAMP -WHERE - id = NEW.id; - -END; - -CREATE INDEX users_username_index ON users (username); \ No newline at end of file +create index users_username_index on users (username); \ No newline at end of file diff --git a/server/migrations/0002_posts_table.sql b/server/migrations/0002_posts_table.sql index 1b9601b..c5d05e5 100644 --- a/server/migrations/0002_posts_table.sql +++ b/server/migrations/0002_posts_table.sql @@ -1,38 +1,12 @@ -CREATE TABLE - IF NOT EXISTS posts ( - id INTEGER PRIMARY KEY NOT NULL, - user_id INTEGER NOT NULL, - content TEXT NOT NULL, - upvotes INTEGER NOT NULL DEFAULT 0, - downvotes INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (id) - ); - --- Create a trigger to set created_at and updated_at on INSERT -CREATE TRIGGER IF NOT EXISTS set_created_at AFTER INSERT ON posts BEGIN -UPDATE posts -SET - created_at = CURRENT_TIMESTAMP -WHERE - id = NEW.id; - -END; - --- Create a trigger to set updated_at on UPDATE -CREATE TRIGGER IF NOT EXISTS set_updated_at AFTER -UPDATE ON posts BEGIN -UPDATE posts -SET - updated_at = CURRENT_TIMESTAMP -WHERE - id = NEW.id; - -END; - -create INDEX IF NOT EXISTS posts_user_id_index ON posts (user_id); - -create INDEX IF NOT EXISTS posts_id_index ON posts (id); - -CREATE INDEX idx_created_at_desc ON posts (created_at DESC); \ No newline at end of file +CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY NOT NULL, + user_id INTEGER NOT NULL, + content TEXT NOT NULL, + upvotes INT NOT NULL DEFAULT 0, + downvotes INT NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) +); +create index IF NOT EXISTS posts_user_id_index on posts (user_id); +create index IF NOT EXISTS posts_id_index on posts (id); \ No newline at end of file diff --git a/server/src/db.rs b/server/src/db.rs index aa7e33d..c2f0f39 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -1,138 +1,36 @@ -use crate::routes::{Post, User}; -use argon2::{ - password_hash::{rand_core::OsRng, SaltString}, - Argon2, PasswordHasher, PasswordVerifier, -}; -use log::{info, warn}; -use sqlx::SqlitePool; +use crate::routes::{NewPost, Post}; +use log::warn; +use sqlx::{Row, SqlitePool}; -// Gets the latest posts from the database, ordered by created_at -pub async fn db_get_latest_posts(pool: &SqlitePool, limit: i64, offset: i64) -> Vec { - sqlx::query_as!( - Post, - "SELECT * FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?", - limit, - offset - ) - .fetch_all(pool) - .await - .unwrap() -} - -// Gets the post with id from the database -pub async fn db_get_post(id: i64, pool: &SqlitePool) -> Option { - sqlx::query_as!(Post, "SELECT * FROM posts WHERE id = ?", id) - .fetch_one(pool) +// Gets all posts from the database +pub async fn db_get_posts(pool: &SqlitePool) -> Vec { + sqlx::query_as!(Post, "SELECT * FROM posts") + .fetch_all(pool) .await - .ok() + .unwrap() } // Inserts a new post to the database -pub async fn db_new_post(userid: i64, content: &str, pool: &SqlitePool) -> Option { - info!("User with id {} submitted a post", userid); - - let insert_query = sqlx::query!( - "INSERT INTO posts (user_id, content) VALUES (?, ?)", - userid, - content +pub async fn db_new_post(post: NewPost, pool: &SqlitePool) -> Option { + let q2 = sqlx::query!( + "INSERT INTO posts (user_id, content) VALUES (1, ?)", + post.content ) .execute(pool) .await; - if insert_query.is_err() { - let s = insert_query.err().unwrap(); + if q2.is_err() { + let s = q2.err().unwrap(); warn!("Error inserting post into database: {}", s); return None; } - // Dips into the database to get the post we just inserted - let post = sqlx::query_as!( + let q = sqlx::query_as!( Post, "SELECT * FROM posts WHERE id = (SELECT MAX(id) FROM posts)" ) .fetch_one(pool) .await .ok()?; - - Some(post) -} - -pub async fn db_user_exists(username: String, pool: &SqlitePool) -> bool { - let exists = sqlx::query!("SELECT username FROM users WHERE username = ?", username) - .fetch_one(pool) - .await - .ok() - .map(|row| row.username); - - exists.is_some() -} - -pub async fn db_user_login(username: String, password: String, pool: &SqlitePool) -> Option { - let username = username.clone(); - let user = sqlx::query_as!(User, "SELECT * FROM users WHERE username = ?", username) - .fetch_one(pool) - .await - .ok()?; - - let phc_password = user.password.clone(); - let phc_password = match argon2::PasswordHash::new(&phc_password) { - Ok(phc_password) => phc_password, - Err(_) => { - warn!( - "Invalid hash for user {} fetched from database (not a valid PHC string)", - username - ); - return None; - } - }; - - let argon2 = Argon2::default(); - let password = password.as_bytes(); - - match argon2.verify_password(password, &phc_password) { - Ok(_) => Some(user), - Err(_) => None, - } -} - -pub async fn db_new_user(username: String, password: String, pool: &SqlitePool) -> Option { - // First check if the user already exists - match db_user_exists(username.clone(), pool).await { - true => { - warn!("User \"{}\" already exists", username); - return None; - } - false => {} - } - - // Unwrapping here because if this fails, we have a serious problem - let phc_hash = Argon2::default() - .hash_password(password.as_bytes(), &SaltString::generate(&mut OsRng)) - .unwrap() - .to_string(); - - // Insert our new user into the database - let insert_query = sqlx::query!( - "INSERT INTO users (username, password) VALUES (?, ?)", - username, - phc_hash - ) - .execute(pool) - .await; - - match insert_query { - Ok(_) => { - info!("User: {} registered", username); - let user = sqlx::query_as!(User, "SELECT * FROM users WHERE username = ?", username) - .fetch_one(pool) - .await - .ok()?; - - Some(user) - } - Err(e) => { - warn!("Error inserting user into database: {}", e); - return None; - } - } + Some(q) } diff --git a/server/src/jwt.rs b/server/src/jwt.rs index d603c4d..2e4bc44 100755 --- a/server/src/jwt.rs +++ b/server/src/jwt.rs @@ -1,3 +1,7 @@ +// use crate::{ +// config::{DAYS_VALID, JWT_SECRET}, +// Claims, +// }; use jsonwebtoken::{ decode, encode, errors::Result as JwtResult, DecodingKey, EncodingKey, Header, Validation, }; @@ -36,8 +40,7 @@ pub fn token_factory(user: &str) -> JwtResult { Ok(token) } -// JwtResult is just a predefined error from the jsonwebtoken crate -// This function is incomplete and should be expanded to check for more things +#[allow(dead_code)] pub fn validate_token(token: &str) -> JwtResult { let token_data = decode::( token, diff --git a/server/src/main.rs b/server/src/main.rs index 6d95008..18e07d2 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -10,8 +10,7 @@ mod jwt; mod routes; mod state; -use routes::{get_posts, login, new_post, post_by_id, register}; -use state::CaptchaState; +use routes::{get_posts, login, new_post, register}; use state::ServerState; #[actix_web::main] @@ -19,7 +18,6 @@ async fn main() -> std::io::Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init(); let data = ServerState::new().await; - let capt_db = CaptchaState::new(); info!("Spinning up server on http://localhost:8080"); HttpServer::new(move || { @@ -31,11 +29,9 @@ async fn main() -> std::io::Result<()> { scope("/api") .service(get_posts) .service(new_post) - .service(post_by_id) .service(login) .service(register) - .app_data(Data::new(data.clone())) - .app_data(Data::new(capt_db.clone())), + .app_data(Data::new(data.clone())), ) .service(Files::new("/", "./public").index_file("index.html")) }) diff --git a/server/src/routes/post.rs b/server/src/routes/post.rs index 42ef85e..cc7970c 100755 --- a/server/src/routes/post.rs +++ b/server/src/routes/post.rs @@ -1,8 +1,7 @@ -use crate::db::{db_get_latest_posts, db_get_post, db_new_post}; -use crate::jwt::validate_token; +use crate::db::db_new_post; use crate::ServerState; -use actix_web::web::{Data, Path, Query}; +use actix_web::web::Data; use actix_web::{get, post, web::Json, HttpResponse, Responder, Result}; use log::info; use serde::{Deserialize, Serialize}; @@ -28,7 +27,6 @@ pub struct Post { pub updated_at: chrono::NaiveDateTime, } -/// The user as it is stored in the database, with all the related metadata #[derive(Debug, Serialize, Deserialize, Clone, FromRow)] pub struct User { pub id: i64, @@ -38,55 +36,17 @@ pub struct User { pub updated_at: chrono::NaiveDateTime, } -// These look like /posts?limit=10&offset=20 in the URL -// Note that these are optional -/// Query parameters for the /posts endpoint -#[derive(Debug, Serialize, Deserialize)] -pub struct QueryParams { - limit: Option, - offset: Option, -} - -/// Gets all posts from the database, query parameters are optional -/// If limit is not specified, it defaults to a sane value #[get("/posts")] -pub async fn get_posts( - query: Query, - state: Data, -) -> Result { - if let (Some(lim), Some(ofs)) = (query.limit, query.offset) { - return Ok(HttpResponse::Ok() - .json(db_get_latest_posts(&state.pool, std::cmp::min(lim, 30), ofs).await)); - } - Ok(HttpResponse::Ok().json(db_get_latest_posts(&state.pool, 30, 0).await)) +pub async fn get_posts(state: Data) -> Result { + let stream = sqlx::query_as!(Post, "SELECT * FROM posts"); + + let posts = stream.fetch_all(&state.pool).await.unwrap(); + Ok(HttpResponse::Ok().json(posts)) } -/// Creates a new post, requires a token in release mode #[post("/posts")] pub async fn new_post(new_post: Json, state: Data) -> Result { - let user_claims = validate_token(&new_post.token); - - if let Err(e) = user_claims { - info!("Error validating token: {}", e); - return Ok(HttpResponse::BadRequest().json("Error")); - } - - // Bail if the token is invalid - let claims = user_claims.unwrap(); - info!("User {:?} created a new post", &claims.sub); - - let content = new_post.content.clone(); - let username = claims.sub.clone(); - - // This one is avoidable if we just store the user id in the token - let userid = sqlx::query!("SELECT id FROM users WHERE username = ?", username) - .fetch_one(&state.pool) - .await - .unwrap() - .id; - - // By now we know that the token is valid, so we can create the post - return match db_new_post(userid, &content, &state.pool).await { + return match db_new_post(new_post.into_inner(), &state.pool).await { Some(post) => { info!("Created post {:?}", post.id); Ok(HttpResponse::Ok().json(post)) @@ -94,12 +54,3 @@ pub async fn new_post(new_post: Json, state: Data) -> Resu None => Ok(HttpResponse::InternalServerError().json("Error")), }; } - -#[get("posts/{id}")] -pub async fn post_by_id(path: Path, state: Data) -> Result { - let id = path.into_inner(); - match db_get_post(id, &state.pool).await { - Some(post) => Ok(HttpResponse::Ok().json(post)), - None => Ok(HttpResponse::NotFound().json("Error")), - } -} diff --git a/server/src/routes/users.rs b/server/src/routes/users.rs index 5b7d999..e5cdec8 100755 --- a/server/src/routes/users.rs +++ b/server/src/routes/users.rs @@ -1,13 +1,14 @@ -use crate::db::{db_new_user, db_user_login}; use crate::jwt::token_factory; -use crate::state::CaptchaState; use crate::ServerState; use actix_web::web::Data; use actix_web::{post, web::Json, HttpResponse, Responder, Result}; -use argon2::password_hash::rand_core::RngCore; +use argon2::password_hash::rand_core::OsRng; +use argon2::password_hash::SaltString; use argon2::password_hash::*; -use biosvg::BiosvgBuilder; +use argon2::Argon2; +use argon2::PasswordHasher; +use argon2::PasswordVerifier; use log::*; use serde::{Deserialize, Serialize}; @@ -18,7 +19,7 @@ pub struct LoginData { } #[derive(Debug, Serialize, Deserialize)] -pub struct LoginResponse { +struct LoginResponse { username: String, token: String, } @@ -35,82 +36,81 @@ pub async fn register( data: Json, state: Data, ) -> Result { - db_new_user(data.username.clone(), data.password.clone(), &state.pool).await; + // First check if the user already exists + let exists = sqlx::query!( + "SELECT username FROM users WHERE username = ?", + data.username + ) + .fetch_one(&state.pool) + .await + .ok() + .map(|row| row.username); + + // Bail out if the user already exists + if exists.is_some() { + info!("User \"{}\" already exists", data.username); + return Ok(HttpResponse::BadRequest().json("Error")); + } + + // Unwrapping here because if this fails, we have a serious problem + let phc_hash = Argon2::default() + .hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng)) + .unwrap() + .to_string(); + + // Insert our new user into the database + sqlx::query!( + "INSERT INTO users (username, password) VALUES (?, ?)", + data.username, + phc_hash + ) + .execute(&state.pool) + .await + .unwrap(); + info!("User: {} registered", data.username); Ok(HttpResponse::Ok().json("User registered")) } #[post("/login")] pub async fn login(data: Json, state: Data) -> Result { - let result = db_user_login(data.username.clone(), data.password.clone(), &state.pool).await; + // let q = "SELECT password FROM users WHERE username = ?"; + // let query = sqlx::query(q).bind(&data.username); + // let result = query.fetch_one(&state.pool).await.ok(); - match result { - Some(_) => { + let uname = data.username.clone(); + let q = sqlx::query!("SELECT password FROM users WHERE username = ?", uname) + .fetch_one(&state.pool) + .await + .ok(); + + if q.is_none() { + info!("User \"{}\" failed to log in", data.username); + return Ok(HttpResponse::BadRequest().json("Error")); + } + + let phc_password = q.unwrap().password; + let phc_password = PasswordHash::new(&phc_password).unwrap_or_else(|_| { + warn!( + "Invalid hash for user {} fetched from database (not a valid PHC string)", + data.username + ); + panic!(); + }); + + match Argon2::default().verify_password(data.password.as_bytes(), &phc_password) { + Ok(_) => { + info!("User {} logged in", data.username); + let token = token_factory(&data.username).unwrap(); + println!("{:?}", token); return Ok(HttpResponse::Ok().json(LoginResponse { username: data.username.clone(), - token: token_factory(&data.username).unwrap(), + token: token, })); } - None => { + Err(_) => { info!("User \"{}\" failed to log in", data.username); return Ok(HttpResponse::BadRequest().json("Error")); } } } - -#[derive(Debug, Serialize, Deserialize)] -pub struct CaptchaResponse { - captcha_svg: String, - captcha_id: i32, -} - -/// Request a captcha from the captcha service -#[post("/captcha")] -pub async fn captcha_request(cstate: Data) -> Result { - // This might block the thread a bit too long - let (answer, svg) = get_captcha(); - - let id = rand_core::OsRng.next_u32() as i32; - - let cresponse = CaptchaResponse { - captcha_svg: svg.clone(), - captcha_id: id, - }; - - // This is bad in about every way i can think of - // It might just be better to hit the database every time, and let the database - // handle rng and maybe set a trigger to delete old captchas - match cstate.capthca_db.lock() { - Ok(mut db) => { - if (db.len() as i32) > 100 { - // To prevent the database from growing too large - // Replace with a proper LRU cache or circular buffer - db.remove(&(id % 100)); // This is terrible - } - db.insert(id, answer.clone()); // We do not care about collisions - return Ok(HttpResponse::Ok().json(cresponse)); - } - Err(_) => { - // This shouldnt happen - error!("Failed to lock captcha database"); - return Ok(HttpResponse::InternalServerError().json("Error")); - } - } -} - -/// Returns a new captcha in the form of a tuple (answer, svg) -fn get_captcha() -> (String, String) { - BiosvgBuilder::new() - .length(4) - .difficulty(6) - .colors(vec![ - // Feel free to change these - "#0078D6".to_string(), - "#aa3333".to_string(), - "#f08012".to_string(), - "#33aa00".to_string(), - "#aa33aa".to_string(), - ]) - .build() - .unwrap() -} diff --git a/server/src/state.rs b/server/src/state.rs index b828927..783b08d 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,27 +1,8 @@ -use std::collections::BTreeMap; -use std::sync::Arc; -use std::sync::Mutex; - -use log::error; -use log::info; -use sqlx::migrate::MigrateDatabase; use sqlx::Pool; use sqlx::Sqlite; +use sqlx::SqlitePool; use sqlx::{self, sqlite}; -#[derive(Clone)] -pub struct CaptchaState { - pub capthca_db: Arc>>, -} - -impl CaptchaState { - pub fn new() -> Self { - Self { - capthca_db: Arc::new(Mutex::new(BTreeMap::new())), - } - } -} - #[derive(Clone)] pub struct ServerState { pub pool: Pool, @@ -32,11 +13,6 @@ impl ServerState { // This is almost certainly bad practice for more reasons than I can count dotenvy::dotenv().ok(); let db_url = dotenvy::var("DATABASE_URL").unwrap_or(":memory:".to_string()); - info!("Using db_url: {}", &db_url); - - if !sqlx::Sqlite::database_exists(&db_url).await.unwrap() { - sqlx::Sqlite::create_database(&db_url).await.unwrap(); - } let pool = sqlite::SqlitePoolOptions::new() .max_connections(5) @@ -46,45 +22,37 @@ impl ServerState { sqlx::migrate!("./migrations").run(&pool).await.unwrap(); - match crate::db::db_new_user("imbus".to_string(), "kartellen1234".to_string(), &pool).await - { - Some(u) => info!("Created default user {}", u.username), - None => error!("Failed to create default user..."), - } - #[cfg(debug_assertions)] - debug_setup(&pool).await.unwrap(); + debug_setup(&pool).await; Self { pool } } } -#[cfg(debug_assertions)] -use sqlx::SqlitePool; - // Inserts a bunch of dummy data into the database // Mostly useful for debugging new posts, as we need to satisfy foreign key constraints. #[cfg(debug_assertions)] -async fn debug_setup(pool: &SqlitePool) -> Result<(), sqlx::Error> { +async fn debug_setup(pool: &SqlitePool) { + use chrono::NaiveDateTime; use sqlx::query; - use crate::db::db_new_user; + let now = NaiveDateTime::from_timestamp(0, 0); - db_new_user("user".to_string(), "pass".to_string(), pool).await; + query!( + "INSERT INTO users (username, password, created_at, updated_at) VALUES ('test', 'test', ?, ?)", + now, + now + ) + .execute(pool) + .await + .unwrap(); - // Check if the demo post already exists - let posted = query!("SELECT * FROM posts WHERE id = 1",) - .fetch_one(pool) - .await - .ok(); - - // If the demo user already has a post, don't insert another one - if !posted.is_some() { - // This requires that the user with id 1 exists in the user table - query!("INSERT INTO posts (user_id, content) VALUES (1, 'Hello world! The demo username is user and the password is pass.')",) - .execute(pool) - .await?; - } - - Ok(()) + query!( + "INSERT INTO posts (user_id, content, created_at, updated_at) VALUES (1, 'Hello world!', ?, ?)", + now, + now + ) + .execute(pool) + .await + .unwrap(); }