Compare commits
2 commits
3e82a5f93c
...
dea4ac1fb3
Author | SHA1 | Date | |
---|---|---|---|
|
dea4ac1fb3 | ||
|
f8dc9cfd29 |
11 changed files with 170 additions and 48 deletions
55
client-solid/src/Footer.tsx
Normal file
55
client-solid/src/Footer.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { JSXElement } from "solid-js";
|
||||||
|
|
||||||
|
export function Footer(): JSXElement {
|
||||||
|
return (
|
||||||
|
<footer class="footer footer-center bottom-0 rounded bg-base-200 p-10 text-base-content">
|
||||||
|
<nav class="grid grid-flow-col gap-4">
|
||||||
|
<a class="link-hover link">About us</a>
|
||||||
|
<a class="link-hover link">Contact</a>
|
||||||
|
</nav>
|
||||||
|
<nav>
|
||||||
|
<div class="grid grid-flow-col gap-4">
|
||||||
|
<a>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="fill-current"
|
||||||
|
>
|
||||||
|
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="fill-current"
|
||||||
|
>
|
||||||
|
<path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="fill-current"
|
||||||
|
>
|
||||||
|
<path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<aside>
|
||||||
|
<p>
|
||||||
|
Copyright © {new Date().getFullYear()} - All right reserved by Swarm
|
||||||
|
Industries Ltd
|
||||||
|
</p>
|
||||||
|
</aside>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
|
@ -15,9 +15,9 @@ export function Posts(): JSXElement {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={!loading()} fallback={loadSpinner()}>
|
<Show when={!loading()} fallback={loadSpinner()}>
|
||||||
<For each={posts()}>
|
{/* <For each={posts()}>
|
||||||
{(post): JSXElement => <PostSegment post={post} />}
|
{(post): JSXElement => <PostSegment post={post} />}
|
||||||
</For>
|
</For> */}
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { GlobalStateProvider } from "./GlobalState";
|
||||||
import { LoginModal } from "./LoginModal";
|
import { LoginModal } from "./LoginModal";
|
||||||
import { Navbar } from "./Navbar";
|
import { Navbar } from "./Navbar";
|
||||||
import { Primary } from "./Primary";
|
import { Primary } from "./Primary";
|
||||||
|
import { Footer } from "./Footer";
|
||||||
|
|
||||||
function Root(): JSXElement {
|
function Root(): JSXElement {
|
||||||
return (
|
return (
|
||||||
|
@ -13,9 +14,10 @@ function Root(): JSXElement {
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<LoginModal />
|
<LoginModal />
|
||||||
<div class="flex w-full flex-col items-center space-y-2 px-2 md:max-w-3xl">
|
<div class="flex w-full min-h-[65vh] flex-col items-center mb-8 space-y-2 px-2 md:max-w-3xl">
|
||||||
<Primary />
|
<Primary />
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</GlobalStateProvider>
|
</GlobalStateProvider>
|
||||||
</>
|
</>
|
||||||
|
@ -25,7 +27,7 @@ function Root(): JSXElement {
|
||||||
function FancyBackground(): JSXElement {
|
function FancyBackground(): JSXElement {
|
||||||
return (
|
return (
|
||||||
<div class="bg-container">
|
<div class="bg-container">
|
||||||
<div class="bg-fancy min-h-screen bg-cover bg-center" />
|
<div class="bg-fancy bg-cover bg-center" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
1
server/Cargo.lock
generated
1
server/Cargo.lock
generated
|
@ -1851,6 +1851,7 @@ dependencies = [
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"lipsum",
|
"lipsum",
|
||||||
"log",
|
"log",
|
||||||
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sled",
|
"sled",
|
||||||
|
|
|
@ -17,6 +17,7 @@ env_logger = "0.10.0"
|
||||||
jsonwebtoken = "8.3.0"
|
jsonwebtoken = "8.3.0"
|
||||||
lipsum = "0.9.0"
|
lipsum = "0.9.0"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
|
rand = "0.8.5"
|
||||||
serde = { version = "1.0.188", features = ["derive"] }
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
serde_json = "1.0.107"
|
serde_json = "1.0.107"
|
||||||
sled = { version = "0.34.7" }
|
sled = { version = "0.34.7" }
|
||||||
|
|
|
@ -6,7 +6,7 @@ use argon2::{
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
// Gets the latest posts from the database, ordered by created_at
|
/// Gets the latest posts from the database, ordered by created_at
|
||||||
pub async fn db_get_latest_posts(pool: &PgPool, limit: i64, offset: i64) -> Vec<Post> {
|
pub async fn db_get_latest_posts(pool: &PgPool, limit: i64, offset: i64) -> Vec<Post> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Post,
|
Post,
|
||||||
|
@ -19,7 +19,7 @@ pub async fn db_get_latest_posts(pool: &PgPool, limit: i64, offset: i64) -> Vec<
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the post with id from the database
|
/// Gets the post with id from the database
|
||||||
pub async fn db_get_post(id: i64, pool: &PgPool) -> Option<Post> {
|
pub async fn db_get_post(id: i64, pool: &PgPool) -> Option<Post> {
|
||||||
sqlx::query_as!(Post, "SELECT * FROM posts WHERE id = $1", id)
|
sqlx::query_as!(Post, "SELECT * FROM posts WHERE id = $1", id)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
|
@ -27,7 +27,7 @@ pub async fn db_get_post(id: i64, pool: &PgPool) -> Option<Post> {
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inserts a new post to the database
|
/// Inserts a new post to the database
|
||||||
pub async fn db_new_post(userid: i64, content: &str, pool: &PgPool) -> Option<Post> {
|
pub async fn db_new_post(userid: i64, content: &str, pool: &PgPool) -> Option<Post> {
|
||||||
info!("User with id {} submitted a post", userid);
|
info!("User with id {} submitted a post", userid);
|
||||||
|
|
||||||
|
@ -57,6 +57,7 @@ pub async fn db_new_post(userid: i64, content: &str, pool: &PgPool) -> Option<Po
|
||||||
Some(post)
|
Some(post)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if the user exists in the database
|
||||||
pub async fn db_user_exists(username: String, pool: &PgPool) -> bool {
|
pub async fn db_user_exists(username: String, pool: &PgPool) -> bool {
|
||||||
let exists = sqlx::query!("SELECT username FROM users WHERE username = $1", username)
|
let exists = sqlx::query!("SELECT username FROM users WHERE username = $1", username)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
|
@ -67,6 +68,7 @@ pub async fn db_user_exists(username: String, pool: &PgPool) -> bool {
|
||||||
exists.is_some()
|
exists.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if the user exists and if the password is correct
|
||||||
pub async fn db_user_login(username: String, password: String, pool: &PgPool) -> Option<User> {
|
pub async fn db_user_login(username: String, password: String, pool: &PgPool) -> Option<User> {
|
||||||
let username = username.clone();
|
let username = username.clone();
|
||||||
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE username = $1", username)
|
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE username = $1", username)
|
||||||
|
@ -95,6 +97,7 @@ pub async fn db_user_login(username: String, password: String, pool: &PgPool) ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new user if the username is not already taken
|
||||||
pub async fn db_new_user(username: String, password: String, pool: &PgPool) -> Option<User> {
|
pub async fn db_new_user(username: String, password: String, pool: &PgPool) -> Option<User> {
|
||||||
// First check if the user already exists
|
// First check if the user already exists
|
||||||
match db_user_exists(username.clone(), pool).await {
|
match db_user_exists(username.clone(), pool).await {
|
||||||
|
|
|
@ -8,10 +8,12 @@ mod db;
|
||||||
mod jwt;
|
mod jwt;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod state;
|
mod state;
|
||||||
|
mod util;
|
||||||
|
|
||||||
use routes::{get_posts, login, new_post, post_by_id, register};
|
use routes::{get_posts, login, new_post, post_by_id, register};
|
||||||
use state::CaptchaState;
|
use state::CaptchaState;
|
||||||
use state::ServerState;
|
use state::ServerState;
|
||||||
|
use util::hex_string;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
|
@ -20,6 +22,15 @@ async fn main() -> std::io::Result<()> {
|
||||||
let data = ServerState::new().await;
|
let data = ServerState::new().await;
|
||||||
let capt_db = CaptchaState::new();
|
let capt_db = CaptchaState::new();
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
for _ in 0..10 {
|
||||||
|
let s = hex_string(10);
|
||||||
|
info!("Adding captcha key: {}", &s);
|
||||||
|
capt_db.capthca_db.lock().unwrap().insert(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
info!("Spinning up server on http://localhost:8080");
|
info!("Spinning up server on http://localhost:8080");
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
|
|
|
@ -5,8 +5,6 @@ use crate::ServerState;
|
||||||
|
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use actix_web::{post, web::Json, HttpResponse, Responder, Result};
|
use actix_web::{post, web::Json, HttpResponse, Responder, Result};
|
||||||
use argon2::password_hash::rand_core::RngCore;
|
|
||||||
use argon2::password_hash::*;
|
|
||||||
use biosvg::BiosvgBuilder;
|
use biosvg::BiosvgBuilder;
|
||||||
use log::*;
|
use log::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -18,7 +16,7 @@ pub struct LoginData {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct LoginResponse {
|
pub struct AuthResponse {
|
||||||
username: String,
|
username: String,
|
||||||
token: String,
|
token: String,
|
||||||
}
|
}
|
||||||
|
@ -34,10 +32,31 @@ pub struct RegisterData {
|
||||||
pub async fn register(
|
pub async fn register(
|
||||||
data: Json<RegisterData>,
|
data: Json<RegisterData>,
|
||||||
state: Data<ServerState>,
|
state: Data<ServerState>,
|
||||||
|
captcha_state: Data<CaptchaState>,
|
||||||
) -> Result<impl Responder> {
|
) -> Result<impl Responder> {
|
||||||
db_new_user(data.username.clone(), data.password.clone(), &state.pool).await;
|
if !captcha_state
|
||||||
info!("User: {} registered", data.username);
|
.capthca_db
|
||||||
Ok(HttpResponse::Ok().json("User registered"))
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.remove(&data.captcha)
|
||||||
|
{
|
||||||
|
info!("User failed to register, captcha was wrong");
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
match db_new_user(data.username.clone(), data.password.clone(), &state.pool).await {
|
||||||
|
Some(user) => {
|
||||||
|
info!("User: {} registered", &user.username);
|
||||||
|
Ok(HttpResponse::Ok().json(AuthResponse {
|
||||||
|
username: user.username.clone(),
|
||||||
|
token: token_factory(&user.username).unwrap(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("User \"{}\" already exists", data.username);
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/login")]
|
#[post("/login")]
|
||||||
|
@ -46,7 +65,7 @@ pub async fn login(data: Json<LoginData>, state: Data<ServerState>) -> Result<im
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
return Ok(HttpResponse::Ok().json(LoginResponse {
|
return Ok(HttpResponse::Ok().json(AuthResponse {
|
||||||
username: data.username.clone(),
|
username: data.username.clone(),
|
||||||
token: token_factory(&data.username).unwrap(),
|
token: token_factory(&data.username).unwrap(),
|
||||||
}));
|
}));
|
||||||
|
@ -67,35 +86,38 @@ pub struct CaptchaResponse {
|
||||||
/// Request a captcha from the captcha service
|
/// Request a captcha from the captcha service
|
||||||
#[post("/captcha")]
|
#[post("/captcha")]
|
||||||
pub async fn captcha_request(cstate: Data<CaptchaState>) -> Result<impl Responder> {
|
pub async fn captcha_request(cstate: Data<CaptchaState>) -> Result<impl Responder> {
|
||||||
|
unimplemented!("Captcha is currently disabled");
|
||||||
|
return Ok(HttpResponse::InternalServerError().json("Error"));
|
||||||
|
|
||||||
// This might block the thread a bit too long
|
// This might block the thread a bit too long
|
||||||
let (answer, svg) = get_captcha();
|
// let (answer, svg) = get_captcha();
|
||||||
|
|
||||||
let id = rand_core::OsRng.next_u32() as i32;
|
// let id = rand_core::OsRng.next_u32() as i32;
|
||||||
|
|
||||||
let cresponse = CaptchaResponse {
|
// let cresponse = CaptchaResponse {
|
||||||
captcha_svg: svg.clone(),
|
// captcha_svg: svg.clone(),
|
||||||
captcha_id: id,
|
// captcha_id: id,
|
||||||
};
|
// };
|
||||||
|
|
||||||
// This is bad in about every way i can think of
|
// 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
|
// 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
|
// handle rng and maybe set a trigger to delete old captchas
|
||||||
match cstate.capthca_db.lock() {
|
// match cstate.capthca_db.lock() {
|
||||||
Ok(mut db) => {
|
// Ok(mut db) => {
|
||||||
if (db.len() as i32) > 100 {
|
// if (db.len() as i32) > 100 {
|
||||||
// To prevent the database from growing too large
|
// // To prevent the database from growing too large
|
||||||
// Replace with a proper LRU cache or circular buffer
|
// // Replace with a proper LRU cache or circular buffer
|
||||||
db.remove(&(id % 100)); // This is terrible
|
// db.remove(&(id % 100)); // This is terrible
|
||||||
}
|
// }
|
||||||
db.insert(id, answer.clone()); // We do not care about collisions
|
// db.insert(id, answer.clone()); // We do not care about collisions
|
||||||
return Ok(HttpResponse::Ok().json(cresponse));
|
// return Ok(HttpResponse::Ok().json(cresponse));
|
||||||
}
|
// }
|
||||||
Err(_) => {
|
// Err(_) => {
|
||||||
// This shouldnt happen
|
// // This shouldnt happen
|
||||||
error!("Failed to lock captcha database");
|
// error!("Failed to lock captcha database");
|
||||||
return Ok(HttpResponse::InternalServerError().json("Error"));
|
// return Ok(HttpResponse::InternalServerError().json("Error"));
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new captcha in the form of a tuple (answer, svg)
|
/// Returns a new captcha in the form of a tuple (answer, svg)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
@ -9,13 +9,14 @@ use sqlx::PgPool;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CaptchaState {
|
pub struct CaptchaState {
|
||||||
pub capthca_db: Arc<Mutex<BTreeMap<i32, String>>>,
|
// pub capthca_db: Arc<Mutex<BTreeMap<i32, String>>>,
|
||||||
|
pub capthca_db: Arc<Mutex<BTreeSet<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CaptchaState {
|
impl CaptchaState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
capthca_db: Arc::new(Mutex::new(BTreeMap::new())),
|
capthca_db: Arc::new(Mutex::new(BTreeSet::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,6 +63,7 @@ impl ServerState {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
async fn debug_setup(pool: &PgPool) -> Result<(), sqlx::Error> {
|
async fn debug_setup(pool: &PgPool) -> Result<(), sqlx::Error> {
|
||||||
use lipsum::lipsum;
|
use lipsum::lipsum;
|
||||||
|
use rand::prelude::*;
|
||||||
use sqlx::query;
|
use sqlx::query;
|
||||||
|
|
||||||
use crate::db::db_new_user;
|
use crate::db::db_new_user;
|
||||||
|
@ -69,26 +71,29 @@ async fn debug_setup(pool: &PgPool) -> Result<(), sqlx::Error> {
|
||||||
db_new_user("user".to_string(), "pass".to_string(), pool).await;
|
db_new_user("user".to_string(), "pass".to_string(), pool).await;
|
||||||
|
|
||||||
// Check if the demo post already exists
|
// Check if the demo post already exists
|
||||||
let posted = query!("SELECT * FROM posts WHERE id = 1",)
|
let no_posts = query!("SELECT * FROM posts WHERE id = 1",)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok()
|
||||||
|
.is_none();
|
||||||
|
|
||||||
// If the demo user already has a post, don't insert another one
|
// If the demo user already has a post, don't insert another one
|
||||||
if !posted.is_some() {
|
if no_posts {
|
||||||
// This requires that the user with id 1 exists in the user table
|
let mut rng = rand::thread_rng();
|
||||||
query!("INSERT INTO posts (user_id, content) VALUES (1, 'Hello world! The demo username is user and the password is pass.')",)
|
|
||||||
.execute(pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for _ in 0..10 {
|
// This requires that the user with id 1 exists in the user table
|
||||||
|
for _ in 0..100 {
|
||||||
query!(
|
query!(
|
||||||
"INSERT INTO posts (user_id, content) VALUES (1, $1)",
|
"INSERT INTO posts (user_id, content) VALUES (1, $1)",
|
||||||
lipsum(50)
|
lipsum(rng.gen_range(10..100))
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(())
|
Ok(())
|
||||||
|
|
3
server/src/util/mod.rs
Normal file
3
server/src/util/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
pub use util::*;
|
19
server/src/util/util.rs
Normal file
19
server/src/util/util.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use rand::{Rng, RngCore};
|
||||||
|
|
||||||
|
// This will do for now
|
||||||
|
pub fn hex_string(length: usize) -> String {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let mut bytes = vec![0u8; length];
|
||||||
|
rng.fill(&mut bytes[..]);
|
||||||
|
bytes.iter().map(|b| format!("{:X}", b)).collect::<String>()[..length].to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_random_hex_string() {
|
||||||
|
let s = hex_string(16);
|
||||||
|
assert_eq!(s.len(), 16);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue