diff --git a/server/Cargo.lock b/server/Cargo.lock index f5d2153..76ce812 100755 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -30,7 +30,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash", - "base64", + "base64 0.21.4", "bitflags 2.4.0", "brotli", "bytes", @@ -238,6 +238,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.5.0" @@ -329,6 +344,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.4" @@ -395,6 +416,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "byteorder" version = "1.4.3" @@ -432,6 +459,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "clap" version = "4.4.5" @@ -501,6 +543,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.9" @@ -986,6 +1034,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.4.0" @@ -1060,6 +1131,29 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.4", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1205,6 +1299,17 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1333,6 +1438,15 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1488,6 +1602,21 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rsa" version = "0.9.2" @@ -1605,8 +1734,10 @@ version = "0.1.0" dependencies = [ "actix-web", "argon2", + "chrono", "clap", "env_logger", + "jsonwebtoken", "log", "serde", "serde_json", @@ -1656,6 +1787,18 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -1831,7 +1974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" dependencies = [ "atoi", - "base64", + "base64 0.21.4", "bitflags 2.4.0", "byteorder", "bytes", @@ -1873,7 +2016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" dependencies = [ "atoi", - "base64", + "base64 0.21.4", "bitflags 2.4.0", "byteorder", "crc", @@ -2171,6 +2314,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.4.1" @@ -2216,6 +2365,70 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.37", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "whoami" version = "1.4.1" @@ -2253,6 +2466,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/server/Cargo.toml b/server/Cargo.toml index f5654ec..08dbc40 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -8,8 +8,10 @@ edition = "2021" [dependencies] actix-web = "4.4.0" argon2 = { version = "0.5.2", features = ["zeroize"] } +chrono = { version = "0.4.31", features = ["serde"] } clap = { version = "4.4.5", features = ["derive"] } env_logger = "0.10.0" +jsonwebtoken = "8.3.0" log = "0.4.20" serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" diff --git a/server/src/jwt.rs b/server/src/jwt.rs new file mode 100755 index 0000000..8111ac1 --- /dev/null +++ b/server/src/jwt.rs @@ -0,0 +1,60 @@ +// use crate::{ +// config::{DAYS_VALID, JWT_SECRET}, +// Claims, +// }; +use jsonwebtoken::{ + decode, encode, errors::Result as JwtResult, DecodingKey, EncodingKey, Header, Validation, +}; +use log::*; +use serde::{Deserialize, Serialize}; + +const DAYS_VALID: i64 = 7; +const JWT_SECRET: &[u8] = "secret".as_bytes(); + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub iss: String, + pub aud: String, + pub iat: usize, + pub exp: usize, +} + +// JwtResult is just a predefined error from the jsonwebtoken crate +pub fn token_factory(user: &str) -> JwtResult { + info!("Issuing JWT token for {}", user); + + let token = encode( + &Header::default(), + &Claims { + sub: user.to_string(), + iss: "localhost".to_string(), + aud: "localhost".to_string(), + iat: chrono::Utc::now().timestamp() as usize, + exp: (chrono::Utc::now() + chrono::Duration::days(DAYS_VALID)).timestamp() as usize, + }, + &EncodingKey::from_secret(JWT_SECRET), + ) + .unwrap(); + + Ok(token) +} + +pub fn validate_token(token: &str) -> JwtResult { + let token_data = decode::( + token, + &DecodingKey::from_secret(JWT_SECRET), + &Validation::default(), + ); + + match token_data { + Ok(token_data) => { + info!("Token validated for {}", token_data.claims.sub); + Ok(token_data.claims) + } + Err(e) => { + error!("Token validation failed: {}", e); + Err(e) + } + } +} diff --git a/server/src/main.rs b/server/src/main.rs index b07876a..5512cbb 100755 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -4,11 +4,12 @@ use actix_web::{web::scope, App, HttpServer}; use log::info; // use uuid::Uuid; +mod jwt; mod routes; mod state; mod types; -use routes::{get_posts, new_post, register, test}; +use routes::{get_posts, login, new_post, register, test}; use sqlx::ConnectOptions; use state::AppState; @@ -33,6 +34,7 @@ async fn main() -> std::io::Result<()> { .service(new_post) .service(routes::vote) .service(test) + .service(login) .service(register) .app_data(Data::new(data.clone())), ) diff --git a/server/src/routes.rs b/server/src/routes.rs index fe4c050..87b9c5b 100755 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -1,7 +1,7 @@ use crate::types::{NewPost, Post}; use crate::AppState; -use actix_web::web::{Data, Path}; +use actix_web::web::{to, Data, Path}; use actix_web::{get, post, web::Json, HttpResponse, Responder, Result}; use argon2::password_hash::SaltString; use log::*; @@ -137,3 +137,55 @@ pub async fn register(data: Json, state: Data) -> Result Ok(HttpResponse::Ok().json("User registered")) } + +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginData { + username: String, + password: String, +} + +use sqlx::Row; + +#[derive(Debug, Serialize, Deserialize)] +struct LoginResponse { + username: String, + token: String, +} + +use crate::jwt::token_factory; + +#[post("/login")] +pub async fn login(data: Json, state: Data) -> Result { + 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(); + if let Some(row) = result { + let phc_from_db = row.get::("password"); + let pwhash = PasswordHash::new(&phc_from_db).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(), &pwhash) { + Ok(some) => { + 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, + })); + // Sign jwt with user claims + } + Err(e) => { + info!("User \"{}\" failed to log in", data.username); + return Ok(HttpResponse::BadRequest().json("Error")); + } + } + } + + Ok(HttpResponse::Ok().json("What happens here???")) +} diff --git a/src/App.tsx b/src/App.tsx index 88bf090..015d6e0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,12 +10,24 @@ import { LoginContext } from './Context.tsx'; // JSX.Element is the return type of every React component function App(): JSX.Element { - const [open, setOpen] = useState(false); + const [loginModalOpen, setLoginModalOpen] = useState(false); + const [currentUser, setCurrentUser] = useState(undefined); + const [userToken, setUserToken] = useState(undefined); + + const loginContextData = { + loginModalOpen: loginModalOpen, + currentUser: currentUser, + userToken: userToken, + setOpen: setLoginModalOpen, + setCurrentUser: setCurrentUser, + setUserToken: setUserToken, + }; + // const loginContextData = { open, setOpen }; // const loginContext = createContext(loginContextData); return ( - +
diff --git a/src/Context.tsx b/src/Context.tsx index 03144f9..5db4eb2 100644 --- a/src/Context.tsx +++ b/src/Context.tsx @@ -2,15 +2,22 @@ import { createContext } from "react" export const TestContext = createContext("Test123") -// export const loginContextData = { open: false, setOpen: (open: boolean): void => {} }; - interface LoginCTX { loginModalOpen: boolean; - setOpen?: (open: boolean) => void; currentUser?: string; userToken?: string; + setOpen?: (open: boolean) => void; + setCurrentUser?: (username: string) => void; + setUserToken?: (token: string) => void; } -const loginContextData = { loginModalOpen: false }; +const loginContextData = { + loginModalOpen: false, + currentUser: undefined, + userToken: undefined, + setOpen: undefined, + setCurrentUser: undefined, + setUserToken: undefined, +}; -export const LoginContext = createContext(loginContextData); \ No newline at end of file +export const LoginContext = createContext(loginContextData); diff --git a/src/Header.tsx b/src/Header.tsx index 3c18eb3..f457ac3 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -1,4 +1,4 @@ -import { AppBar, Button, ButtonGroup, Link, Typography } from "@mui/material"; +import { AppBar, Button, ButtonGroup, Grid, Link, Typography } from "@mui/material"; import Box from "@mui/material/Box"; import AcUnitIcon from '@mui/icons-material/AcUnit'; import { cyan } from "@mui/material/colors"; @@ -13,14 +13,17 @@ function LoginDisplay({ sx }: { sx?: React.CSSProperties }): JSX.Element { const handleLogin = (): void => { console.log("Login button pressed") - console.log(loginCtx); - loginCtx.setOpen?.(true); // If the loginCtx.setOpen is defined, call it with true as the argument - console.log(loginCtx); + if (loginCtx.currentUser == undefined) { + loginCtx.setOpen?.(true); // If the loginCtx.setOpen is defined, call it with true as the argument + } else { + loginCtx.setCurrentUser?.(undefined); + loginCtx.setUserToken?.(undefined); + } } if (loginCtx.currentUser != undefined) { return ( - + Logged in as: @@ -34,7 +37,7 @@ function LoginDisplay({ sx }: { sx?: React.CSSProperties }): JSX.Element { } return ( - + ) @@ -43,11 +46,11 @@ function LoginDisplay({ sx }: { sx?: React.CSSProperties }): JSX.Element { function Header({ sx }: { sx?: React.CSSProperties }): JSX.Element { return ( - - - - - + + + + + ) } @@ -59,12 +62,12 @@ function HeaderLogo({ clickable, sx }: { clickable?: boolean, sx?: React.CSSProp return ( @@ -80,7 +83,7 @@ function NavButtons({ sx }: { sx?: React.CSSProperties }): JSX.Element { {["Home", "New"].map((typename): JSX.Element => { return ( - + ) })} diff --git a/src/LoginDialog.tsx b/src/LoginDialog.tsx index b36b66b..87c1b2d 100644 --- a/src/LoginDialog.tsx +++ b/src/LoginDialog.tsx @@ -5,7 +5,7 @@ import { DialogContentText } from "@mui/material"; import { DialogActions } from "@mui/material"; import { Button } from "@mui/material"; import { TextField } from "@mui/material"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useContext } from "react"; import { LoginContext } from "./Context"; // import { TestContext } from "./Context"; @@ -16,32 +16,79 @@ interface RegisterData { captcha: string, } +interface LoginData { + username: string, + password: string, +} + function sendRegister(data: RegisterData): void { console.log(JSON.stringify(data)); - fetch("/api/register", { - method: "POST", + fetch("/api/register", { + method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data) + body: JSON.stringify(data) }); } +interface LoginResponse { + username: string, + token: string, +} + +async function sendLogin(data: LoginData): Promise { + // console.log(JSON.stringify(data)); + const response_promise = await fetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data) + }); + + const logindata = await response_promise.json(); + return (logindata as LoginResponse); +} + function LoginDialog(): JSX.Element { - // const [open, setOpen] = useState(openState); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); - // const test = useContext(TestContext); const loginCTX = useContext(LoginContext); + const setLoggedInAs = (username: string, token: string): void => { + loginCTX.setCurrentUser?.(username); + loginCTX.setUserToken?.(token); + handleClose(); + } + + // const setLogOut = (): void => { + // loginCTX.setCurrentUser?.(undefined); + // loginCTX.setUserToken?.(undefined); + // localStorage.removeItem("loginState"); + // } + const handleClose = (): void => { loginCTX.setOpen?.(false); + setUsername(""); + setPassword(""); }; - const handleLogin = (): void => { - console.log(username, password); + // Check for localStorage login state + useEffect((): void => { + const loginState = JSON.parse(localStorage.getItem("loginState")||"{}"); + if (loginState.username && loginState.token) { + setLoggedInAs(loginState.username, loginState.token); + } + }, []); + + const handleLogin = async (): Promise => { + const response = await sendLogin({ username: username, password: password }); + if (response && response.username && response.token) { + setLoggedInAs(response.username, response.token); + localStorage.setItem("loginState", JSON.stringify(response)); + } + }; - const handleRegister = (): void => { + const handleRegister = (): void => { sendRegister({ username: username, password: password, captcha: "test" }); } diff --git a/src/style/index.scss b/src/style/index.scss index 802c5d9..1615020 100644 --- a/src/style/index.scss +++ b/src/style/index.scss @@ -1,43 +1 @@ -// :root { -// font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; -// line-height: 1.5; -// font-weight: 400; - -// color-scheme: light dark; -// color: rgba(255, 255, 255, 0.87); -// background-color: #242424; -// } - -// body * { -// box-sizing: border-box; -// margin: 0; -// } - -// max-width: 1280px; -// margin: 0 auto; -// padding: 2rem; -// text-align: center; -// } - -// // @media (prefers-reduced-motion: no-preference) {} - -// body { -// margin: 0; -// // display: flex; -// min-width: 320px; -// min-height: 100vh; -// place-items: center; -// } - -// @media (prefers-color-scheme: light) { -// :root { -// color: #213547; -// background-color: #ffffff; -// } -// a:hover { -// color: #747bff; -// } -// button { -// background-color: #f9f9f9; -// } -// } +// No free standing CSS \ No newline at end of file