Working example

This commit is contained in:
Imbus 2023-10-10 17:12:47 +02:00
parent 83345afe04
commit f514fd741c
10 changed files with 446 additions and 81 deletions

228
server/Cargo.lock generated
View file

@ -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"

View file

@ -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"

60
server/src/jwt.rs Executable file
View file

@ -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<String> {
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<Claims> {
let token_data = decode::<Claims>(
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)
}
}
}

View file

@ -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())),
)

View file

@ -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<RegisterData>, state: Data<AppState>) -> 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<LoginData>, state: Data<AppState>) -> Result<impl Responder> {
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::<String, _>("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???"))
}

View file

@ -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<string | undefined>(undefined);
const [userToken, setUserToken] = useState<string | undefined>(undefined);
const loginContextData = {
loginModalOpen: loginModalOpen,
currentUser: currentUser,
userToken: userToken,
setOpen: setLoginModalOpen,
setCurrentUser: setCurrentUser,
setUserToken: setUserToken,
};
// const loginContextData = { open, setOpen };
// const loginContext = createContext(loginContextData);
return (
<LoginContext.Provider value={{ loginModalOpen: open, setOpen }} >
<LoginContext.Provider value={loginContextData} >
<Box flexDirection={"column"} display={"flex"} sx={{ width: "100%", minHeight: "100vh", backgroundColor:"background.default"}}>
<Header />
<LoginDialog />

View file

@ -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<LoginCTX>(loginContextData);

View file

@ -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);
if (loginCtx.currentUser == undefined) {
loginCtx.setOpen?.(true); // If the loginCtx.setOpen is defined, call it with true as the argument
console.log(loginCtx);
} else {
loginCtx.setCurrentUser?.(undefined);
loginCtx.setUserToken?.(undefined);
}
}
if (loginCtx.currentUser != undefined) {
return (
<Box sx={{ width: 1, display: "flex", flexDirection: "column", justifyContent: "center", ...sx }}>
<Box sx={{ textAlign: "right", ...sx }}>
<Typography color={"#808080"} sx={{ textAlign: "right" }}>
Logged in as:
</Typography>
@ -34,7 +37,7 @@ function LoginDisplay({ sx }: { sx?: React.CSSProperties }): JSX.Element {
}
return (
<Box sx={{ width: 1, display: "flex", flexDirection: "column", justifyContent: "center", ...sx }}>
<Box sx={{ textAlign: "right", ...sx }}>
<Button variant="text" startIcon={<AccountCircleIcon />} onClick={(): void => handleLogin()}>Login</Button>
</Box>
)
@ -43,11 +46,11 @@ function LoginDisplay({ sx }: { sx?: React.CSSProperties }): JSX.Element {
function Header({ sx }: { sx?: React.CSSProperties }): JSX.Element {
return (
<AppBar position='static' sx={{ p: 1, px: 3, display: 'flex', flexDirection: 'row', justifyContent: 'space-evenly', ...sx }}>
<Box sx={{ width: 1, px: 3, display: "flex", flexDirection: "row", flexGrow: 1, maxWidth: "1200px", justifyContent: "space-between" }}>
<HeaderLogo clickable/>
<NavButtons />
<LoginDisplay />
</Box>
<Grid container px={2} spacing={2} sx={{ flexGrow: 1, display: "flex", maxWidth: "1200px", flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
<Grid item xs={12} sm={6} md={4} lg={4}><HeaderLogo clickable /></Grid>
<Grid item xs={12} sm={6} md={4} lg={4}><NavButtons /></Grid>
<Grid item xs={12} sm={6} md={4} lg={4}><LoginDisplay /></Grid>
</Grid>
</AppBar >)
}
@ -59,7 +62,7 @@ function HeaderLogo({ clickable, sx }: { clickable?: boolean, sx?: React.CSSProp
return (
<Box sx={{
width: 1,
width: "fit-content",
display: 'flex',
flexDirection: 'row',
alignItems: 'center',

View file

@ -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,6 +16,11 @@ interface RegisterData {
captcha: string,
}
interface LoginData {
username: string,
password: string,
}
function sendRegister(data: RegisterData): void {
console.log(JSON.stringify(data));
fetch("/api/register", {
@ -25,20 +30,62 @@ function sendRegister(data: RegisterData): void {
});
}
interface LoginResponse {
username: string,
token: string,
}
async function sendLogin(data: LoginData): Promise<LoginResponse> {
// 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<void> => {
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 => {

View file

@ -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