Initial commit
This commit is contained in:
commit
f6797049af
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "tracking_pixel"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
askama = "0.12.0"
|
||||
base64 = "0.21.0"
|
||||
chrono = "0.4.24"
|
||||
crypto-hash = "0.3.4"
|
||||
oneshot = "0.1.5"
|
||||
rand = "0.8.5"
|
||||
rouille="3.6.1"
|
||||
rusqlite = { version = "0.29.0", features = ["bundled", "chrono"] }
|
||||
thiserror = "1.0.40"
|
|
@ -0,0 +1,11 @@
|
|||
- implement limit on number of shown hits
|
||||
- date on pixel
|
||||
- fix redirect
|
||||
- pragma foreign_keys
|
||||
- session
|
||||
- ip blacklist
|
||||
- clear/delete hits
|
||||
- move db operations out of db_delegate
|
||||
- ORM?
|
||||
- async?
|
||||
- pipelining?
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
Hallo
|
||||
</h1>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
After Width: | Height: | Size: 161 B |
|
@ -0,0 +1,101 @@
|
|||
use rusqlite::{Connection};
|
||||
use std::{sync::mpsc::{Sender, Receiver, channel, SendError}, thread::{self, JoinHandle}, any::Any};
|
||||
|
||||
enum DBMessage<T> {
|
||||
HangUp,
|
||||
Message(T)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DBSender<MsgT> {
|
||||
sender: Sender<DBMessage<MsgT>>
|
||||
}
|
||||
|
||||
// #[derive(Clone)] requires MsgT to be Clone, but that's not necessary
|
||||
impl<MsgT> Clone for DBSender<MsgT> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {sender: self.sender.clone()}
|
||||
}
|
||||
}
|
||||
|
||||
impl<MsgT> DBSender<MsgT> {
|
||||
pub fn send(&self, msg: MsgT) -> Result<(), SendError<MsgT>>{
|
||||
if let Err(err) = self.sender.send(DBMessage::Message(msg)) {
|
||||
if let DBMessage::Message(msg) = err.0 {
|
||||
Err(SendError(msg))
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn hang_up(&self) -> Result<(), SendError<()>> {
|
||||
if let Err(_) = self.sender.send(DBMessage::HangUp) {
|
||||
Err(SendError(()))
|
||||
}
|
||||
else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DBConnection<MsgT: Send + 'static>
|
||||
{
|
||||
pub sender: DBSender<MsgT>,
|
||||
join_handle: Option<JoinHandle<()>>
|
||||
}
|
||||
|
||||
|
||||
impl<MsgT: Send + 'static> DBConnection<MsgT>
|
||||
{
|
||||
pub fn init<F: Fn(MsgT, &Connection) + Send + 'static>(connection: Connection, op: F) -> Self {
|
||||
let (sender, receiver) = channel();
|
||||
|
||||
let join_handle = thread::spawn(move || Self::db_thread(receiver, connection, op));
|
||||
let sender = DBSender { sender };
|
||||
|
||||
Self {sender, join_handle: Some(join_handle)}
|
||||
}
|
||||
|
||||
fn db_thread<F: Fn(MsgT, &Connection) + Send>(receiver: Receiver<DBMessage<MsgT>>, connection: Connection, op: F) {
|
||||
while let Ok(db_msg) = receiver.recv() {
|
||||
// _ = connection.execute("INSERT INTO hits VALUES(?1, ?2, ?3, ?4);", params![hit.pixel_id, hit.ip, hit.user_agent, hit.date]).unwrap();
|
||||
if let DBMessage::Message(msg) = db_msg {
|
||||
op(msg, &connection);
|
||||
}
|
||||
else if let DBMessage::HangUp = db_msg {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// execute last messages in buffer
|
||||
for db_msg in receiver.try_iter() {
|
||||
if let DBMessage::Message(msg) = db_msg {
|
||||
op(msg, &connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
// static is implied, but included for clearness
|
||||
pub fn finish(&mut self) -> Result<(), Box<dyn Any + Send + 'static>> {
|
||||
// let Self{sender, join_handle} = self;
|
||||
if let Err(err) = self.sender.hang_up() {
|
||||
return Err(Box::new(err));
|
||||
}
|
||||
if let Some(handle) = self.join_handle.take() {
|
||||
handle.join()
|
||||
}
|
||||
else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<MsgT: Send> Drop for DBConnection<MsgT> {
|
||||
fn drop(&mut self) {
|
||||
self.finish().unwrap();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
use std::{fs, sync::{Mutex}};
|
||||
|
||||
use askama::Template;
|
||||
use rouille::{Response, router, input, Request, try_or_400, post_input};
|
||||
|
||||
|
||||
use pixel::{Hit, PixelManager, PixelManagerDelegate, TrackingPixel, User};
|
||||
|
||||
mod db;
|
||||
mod pixel;
|
||||
|
||||
|
||||
fn main() {
|
||||
let white_1x1_pixel_png = fs::read("assets/white_pixel.png").unwrap();
|
||||
|
||||
let pixel_manager = PixelManager::build("app.db").unwrap();
|
||||
|
||||
let pixel_manager_delegate = PixelManagerDelegate::new(&pixel_manager);
|
||||
// let pixel_id = pixel_manager.create_pixel("Hallo mac").unwrap();
|
||||
// print!("{pixel_id}");
|
||||
// pixel_manager.register_hit(pixel_id, "ai pie", "joeser eedsjent", chrono::offset::Utc::now());
|
||||
|
||||
let delegate_mutex = Mutex::new(pixel_manager_delegate);
|
||||
|
||||
rouille::start_server("0.0.0.0:3737", move |request| {
|
||||
router!(request,
|
||||
(GET) ["/manage"] => {
|
||||
let delegate = delegate_mutex.lock().unwrap().clone();
|
||||
// panicking on poisoned mutex is acceptable
|
||||
manage_index(request, &delegate)
|
||||
},
|
||||
(GET) ["/manage/pixel/{pixel_id}", pixel_id: String] => {
|
||||
let delegate = delegate_mutex.lock().unwrap().clone();
|
||||
manage_pixel(&pixel_id, request, &delegate)
|
||||
},
|
||||
(POST) ["/manage/pixel/create"] => {
|
||||
let delegate = delegate_mutex.lock().unwrap().clone();
|
||||
create_pixel(request, &delegate)
|
||||
},
|
||||
(POST) ["/manage/pixel/delete"] => {
|
||||
let delegate = delegate_mutex.lock().unwrap().clone();
|
||||
delete_pixel(request, &delegate)
|
||||
},
|
||||
(GET) ["/img/{id}/white.png", id: String] => {
|
||||
let delegate = delegate_mutex.lock().unwrap().clone();
|
||||
register_hit(&id, request, &delegate);
|
||||
Response::from_data("image/png", white_1x1_pixel_png.clone())
|
||||
},
|
||||
_ => {
|
||||
println!("404: {:#?}", request.url());
|
||||
Response::empty_404()
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
struct IndexTemplate<'a> {
|
||||
username: &'a str,
|
||||
pixels: Vec<(String, String)>
|
||||
}
|
||||
|
||||
impl<'a> IndexTemplate<'a> {
|
||||
fn new(username: &'a str, pixels: &Vec<TrackingPixel>) -> Self {
|
||||
Self {
|
||||
username,
|
||||
pixels: pixels.iter().map(|pix| (pix.name.clone(), pix.url_safe_encode())).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "manage/pixel.html")]
|
||||
struct PixelTemplate {
|
||||
pixel_name: String,
|
||||
hits: Vec<(String, String, String)>,
|
||||
pixel_url: String,
|
||||
pixel_id: String
|
||||
}
|
||||
|
||||
impl PixelTemplate {
|
||||
fn new(pixel: &TrackingPixel, hits: &Vec<Hit>) -> Self {
|
||||
Self {
|
||||
pixel_name: pixel.name.clone(),
|
||||
hits: hits.iter().map(|hit| (hit.date.to_string(), hit.ip.clone(), hit.user_agent.clone())).collect(),
|
||||
pixel_url: get_url_for_pixel(&pixel),
|
||||
pixel_id: pixel.url_safe_encode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn manage_index(request: &Request, manager_delegate: &PixelManagerDelegate) -> Response {
|
||||
let user = match auth_user(request, &manager_delegate) {
|
||||
Ok(user) => user,
|
||||
Err(response) => return response
|
||||
};
|
||||
|
||||
match manager_delegate.get_manager().get_pixels_for_user(&user) {
|
||||
Ok(pixels) => {
|
||||
let index_template = IndexTemplate::new(&user.username, &pixels);
|
||||
match index_template.render() {
|
||||
Ok(page) => Response::html(page),
|
||||
Err(_) => Response::text("Page error").with_status_code(500)
|
||||
}
|
||||
},
|
||||
Err(_) => Response::text("DB error").with_status_code(500)
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_user(request: &Request, manager_delegate: &PixelManagerDelegate) -> Result<User, Response> {
|
||||
let auth = match input::basic_http_auth(request) {
|
||||
Some(a) => a,
|
||||
None => return Err(Response::basic_http_auth_login_required("manage"))
|
||||
};
|
||||
match manager_delegate.get_manager().get_user(&auth.login, &auth.password) {
|
||||
Ok(user) => Ok(user),
|
||||
Err(_) => return Err(Response::basic_http_auth_login_required("manage"))
|
||||
}
|
||||
}
|
||||
|
||||
fn manage_pixel(pixel_id: &str, request: &Request, manager_delegate: &PixelManagerDelegate) -> Response {
|
||||
let user = match auth_user(request, &manager_delegate) {
|
||||
Ok(user) => user,
|
||||
Err(response) => return response
|
||||
};
|
||||
|
||||
let pixel = match manager_delegate.get_manager().get_pixel_from_encoded(pixel_id, Some(&user)) {
|
||||
Ok(pixel_opt) => {
|
||||
match pixel_opt {
|
||||
Some(pixel) => pixel,
|
||||
None => return Response::text("Unknown pixel").with_status_code(500)
|
||||
}
|
||||
},
|
||||
Err(_) => return Response::text("Encoding error").with_status_code(500)
|
||||
};
|
||||
|
||||
let hits = match manager_delegate.get_manager().get_hits_for_pixel(&pixel, Some(&user)) {
|
||||
Ok(hits) => hits,
|
||||
Err(_) => return Response::text("DB error").with_status_code(500)
|
||||
};
|
||||
|
||||
let pixel_template = PixelTemplate::new(&pixel, &hits);
|
||||
|
||||
match pixel_template.render() {
|
||||
Ok(page) => Response::html(page),
|
||||
Err(_) => Response::text("Page error").with_status_code(500)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_pixel(request: &Request, manager_delegate: &PixelManagerDelegate) -> Response {
|
||||
let user = match auth_user(request, &manager_delegate) {
|
||||
Ok(user) => user,
|
||||
Err(response) => return response
|
||||
};
|
||||
let input = try_or_400!(post_input!(request, {
|
||||
pixel_name: String
|
||||
}));
|
||||
let _pixel = match manager_delegate.get_manager().create_pixel(&input.pixel_name, &user) {
|
||||
Ok(pixel) => pixel,
|
||||
Err(_) => return Response::empty_400()
|
||||
};
|
||||
Response::redirect_303("/manage")
|
||||
}
|
||||
|
||||
fn delete_pixel(request: &Request, manager_delegate: &PixelManagerDelegate) -> Response {
|
||||
let user = match auth_user(request, &manager_delegate) {
|
||||
Ok(user) => user,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
let input = try_or_400!(post_input!(request, {
|
||||
pixel_id: String
|
||||
}));
|
||||
|
||||
let pixel = match manager_delegate.get_manager().get_pixel_from_encoded(&input.pixel_id, Some(&user)) {
|
||||
Ok(pixel) => pixel,
|
||||
Err(_) => return Response::text("DB error").with_status_code(500)
|
||||
};
|
||||
if let Some(pixel) = pixel {
|
||||
if manager_delegate.get_manager().delete_pixel(pixel, Some(&user)).is_err() {
|
||||
return Response::text("DB error").with_status_code(500);
|
||||
}
|
||||
} else {
|
||||
return Response::empty_400(); // could not find pixel
|
||||
}
|
||||
Response::redirect_303("/manage")
|
||||
}
|
||||
|
||||
fn register_hit(pixel_id: &str, request: &Request, manager_delegate: &PixelManagerDelegate) {
|
||||
let ua = request.header("User-Agent").unwrap_or_default();
|
||||
let ip = request.header("X-Forwarded-For").unwrap_or_default();
|
||||
_ = manager_delegate.get_manager().register_hit_with_encoded_pixel(pixel_id, &ip, ua, chrono::offset::Utc::now());
|
||||
}
|
||||
|
||||
|
||||
fn get_url_for_pixel(pixel: &TrackingPixel) -> String {
|
||||
format!("https://track.willem.page/img/{}/white.png", pixel.url_safe_encode())
|
||||
}
|
|
@ -0,0 +1,482 @@
|
|||
use std::sync::mpsc;
|
||||
|
||||
use crate::db::{DBConnection, DBSender};
|
||||
|
||||
use base64::Engine;
|
||||
use chrono::{DateTime, Utc};
|
||||
use crypto_hash::{Algorithm, hex_digest};
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Username or password does not exist in database")]
|
||||
InvalidUserData(),
|
||||
#[error("Database did not respond")]
|
||||
RecvError(#[from] oneshot::RecvError),
|
||||
#[error("Could not send data to database")]
|
||||
SendError(#[from] mpsc::SendError<DBConnectionMessage>),
|
||||
#[error("Timeout receiving data from database")]
|
||||
TimeoutError(#[from] oneshot::RecvTimeoutError),
|
||||
#[error("User is not authenticated")]
|
||||
UserNotAuthed(),
|
||||
#[error("No user with this username and password")]
|
||||
InvalidCredentials(),
|
||||
#[error("Data was not added to database correctly")]
|
||||
DBError(),
|
||||
#[error("The username for this user already exists")]
|
||||
NewUserAlreadyExists(),
|
||||
#[error("Invalid encoding in ID")]
|
||||
InvalidEncodedID(#[from] base64::DecodeError),
|
||||
#[error("Pixel not found in users pixels")]
|
||||
PixelNotFound()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DBConnectionMessage {
|
||||
RegisterHit(Hit),
|
||||
CreatePixel(TrackingPixel),
|
||||
DeletePixel(i64),
|
||||
AuthUser(User, oneshot::Sender<bool>),
|
||||
GetUserFromName(String, oneshot::Sender<Option<User>>),
|
||||
GetUserFromId(i64, oneshot::Sender<Option<User>>),
|
||||
CreateUser(User),
|
||||
DeleteUser(i64),
|
||||
GetHitsForPixel(TrackingPixel, oneshot::Sender<Option<Vec<Hit>>>, Option<User>),
|
||||
GetPixelsForUser(User, oneshot::Sender<Option<Vec<TrackingPixel>>>),
|
||||
GetPixel(i64, Option<User>, oneshot::Sender<Option<TrackingPixel>>)
|
||||
} // messy, there is probably a better way to do this but it works so oh well
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrackingPixel {
|
||||
id: i64, // ID may not be omitted when creating new pixel
|
||||
pub name: String,
|
||||
user_id: i64
|
||||
}
|
||||
|
||||
impl TrackingPixel {
|
||||
pub fn url_safe_encode(&self) -> String {
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.id.to_be_bytes())
|
||||
}
|
||||
|
||||
fn url_safe_decode_id(encoded_id: &str) -> Result<i64, base64::DecodeError> {
|
||||
let bytes_vec = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded_id)?;
|
||||
let mut bytes_arr = [0; 8];
|
||||
|
||||
for i in 0..8 {
|
||||
bytes_arr[i] = bytes_vec.get(i).cloned().unwrap_or_default();
|
||||
}
|
||||
|
||||
Ok(i64::from_be_bytes(bytes_arr))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Hit {
|
||||
id: Option<i64>, // ID is omitted when creating new hit
|
||||
pixel_id: i64,
|
||||
pub ip: String,
|
||||
pub user_agent: String,
|
||||
pub date: DateTime<Utc>
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct User {
|
||||
id: Option<i64>, // ID is omitted when creating new user
|
||||
pub username: String,
|
||||
pass_hash: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PixelManager {
|
||||
db_connection_sender: DBSender<DBConnectionMessage>,
|
||||
db_connection: Option<DBConnection<DBConnectionMessage>> // only original manager holds the connection, clones in delegate do not
|
||||
}
|
||||
|
||||
impl PixelManager {
|
||||
pub fn build(db_file: &str) -> Option<Self> {
|
||||
let conn = Connection::open(db_file);
|
||||
if conn.is_err() {
|
||||
return None;
|
||||
}
|
||||
let db_connection = DBConnection::init(conn.unwrap(), Self::db_delegate);
|
||||
|
||||
Some(Self{ db_connection_sender: db_connection.sender.clone(), db_connection: Some(db_connection) })
|
||||
}
|
||||
|
||||
fn db_delegate(msg: DBConnectionMessage, conn: &Connection) {
|
||||
_ = match msg { // beware, code folding recommended
|
||||
DBConnectionMessage::CreatePixel(pixel) =>
|
||||
conn.execute("INSERT INTO pixels (id, name, user_id) VALUES (?1, ?2, ?3)", params![pixel.id, pixel.name, pixel.user_id]),
|
||||
DBConnectionMessage::RegisterHit(hit) =>
|
||||
conn.execute("INSERT INTO hits (pixel_id, ip, user_agent, date) VALUES (?1, ?2, ?3, ?4)", params![hit.pixel_id, hit.ip, hit.user_agent, hit.date]),
|
||||
DBConnectionMessage::DeletePixel(pixel_id) =>
|
||||
conn.execute("DELETE FROM pixels WHERE id = ?1", [pixel_id]),
|
||||
DBConnectionMessage::AuthUser(user, response_sender) => {
|
||||
let result: Result<i64, rusqlite::Error>;
|
||||
if let User{ id: Some(id), username, pass_hash} = user {
|
||||
result = conn.query_row("SELECT id FROM users WHERE id = ?1 AND username = ?2 AND pass_hash = ?3", params![id, username, pass_hash],
|
||||
|row| row.get(0));
|
||||
match result.optional() {
|
||||
Ok(id) => {
|
||||
if let Some(_) = id {
|
||||
_ = response_sender.send(true);
|
||||
} else {
|
||||
_ = response_sender.send(false);
|
||||
}
|
||||
Ok(1) // 1 row affected
|
||||
}
|
||||
Err(e) => {Err(e)},
|
||||
}
|
||||
} else { // invalid user object
|
||||
Ok(0) // 0 rows affected
|
||||
}
|
||||
},
|
||||
DBConnectionMessage::GetUserFromName(username, response_sender) => {
|
||||
let result = conn.query_row("SELECT id, username, pass_hash FROM users WHERE username = ?1", [username],
|
||||
|row| Ok(
|
||||
User{
|
||||
id: row.get(0)?,
|
||||
username: row.get(1)?,
|
||||
pass_hash: row.get(2)?
|
||||
}));
|
||||
match result.optional() {
|
||||
Ok(user) => {
|
||||
if let Some(user) = user {
|
||||
_ = response_sender.send(Some(user));
|
||||
Ok(1) // 1 row affected
|
||||
}
|
||||
else {
|
||||
_ = response_sender.send(None);
|
||||
Ok(0) // no rows affected
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
_ = response_sender.send(None);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
DBConnectionMessage::GetUserFromId(id, response_sender) => {
|
||||
let result = conn.query_row("SELECT id, username, pass_hash FROM users WHERE id = ?1", [id],
|
||||
|row| Ok(
|
||||
User{
|
||||
id: row.get(0)?,
|
||||
username: row.get(1)?,
|
||||
pass_hash: row.get(2)?
|
||||
}));
|
||||
match result.optional() {
|
||||
Ok(user) => {
|
||||
if let Some(user) = user {
|
||||
_ = response_sender.send(Some(user));
|
||||
Ok(1) // 1 row affected
|
||||
}
|
||||
else {
|
||||
_ = response_sender.send(None);
|
||||
Ok(0) // no rows affected
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
_ = response_sender.send(None);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
DBConnectionMessage::CreateUser(user) =>
|
||||
conn.execute("INSERT INTO users (username, pass_hash) VALUES (?1, ?2)", params![user.username, user.pass_hash]),
|
||||
DBConnectionMessage::DeleteUser(id) =>
|
||||
conn.execute("DELETE from users WHERE id = ?1", [id]),
|
||||
DBConnectionMessage::GetHitsForPixel(pixel, response_sender, user) => 'gethitsforpixel: {
|
||||
let mut stmt = conn.prepare("SELECT id, pixel_id, ip, user_agent, date FROM hits WHERE pixel_id = ?1").unwrap();
|
||||
if let Some(user) = user {
|
||||
if let Some(user_id) = user.id {
|
||||
// check if pixel belongs to user
|
||||
let pixel_id: Result<i64, _> = conn.query_row("SELECT id FROM pixels WHERE id = ?1 AND user_id = ?2", [pixel.id, user_id], |row| {
|
||||
Ok(row.get(0)?)
|
||||
});
|
||||
if !pixel_id.is_ok() {
|
||||
_ = response_sender.send(None);
|
||||
break 'gethitsforpixel Ok(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
// if this fails something is very wrong with the underlying connection, so panicking the thread is perfectly acceptable
|
||||
let mapped_rows = stmt.query_map([pixel.id], |row| {
|
||||
Ok(Hit {
|
||||
id: Some(row.get(0)?),
|
||||
pixel_id: row.get(1)?,
|
||||
ip: row.get(2)?,
|
||||
user_agent: row.get(3)?,
|
||||
date: row.get(4)?,
|
||||
})
|
||||
}); // processing on the DB thread is a bad idea, but this really isn't very complex and I don't really care
|
||||
|
||||
match mapped_rows {
|
||||
Ok(hits) => {
|
||||
let hits: Vec<Hit> = hits.filter_map(|hit| hit.ok()).collect();
|
||||
let rows_affected = hits.len();
|
||||
_ = response_sender.send(Some(hits));
|
||||
Ok(rows_affected)
|
||||
},
|
||||
Err(e) => {
|
||||
_ = response_sender.send(None);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
DBConnectionMessage::GetPixelsForUser(user, response_sender) => {
|
||||
let mut stmt = conn.prepare("SELECT id,name,user_id FROM pixels WHERE user_id = ?1").unwrap(); // should't fail but if it does connection is bad and panicking is acceptable
|
||||
let mapped_rows = stmt.query_map([user.id], |row| {
|
||||
Ok(TrackingPixel {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
user_id: row.get(2)?
|
||||
})
|
||||
});
|
||||
|
||||
match mapped_rows {
|
||||
Ok(pixels) => {
|
||||
let pixels: Vec<TrackingPixel> = pixels.filter_map(|pixel| pixel.ok()).collect();
|
||||
let rows_affected = pixels.len();
|
||||
_ = response_sender.send(Some(pixels));
|
||||
Ok(rows_affected)
|
||||
},
|
||||
Err(e) => {
|
||||
_ = response_sender.send(None);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
DBConnectionMessage::GetPixel(id, user, response_sender) => {
|
||||
let mut stmt = conn.prepare("SELECT id, name, user_id FROM pixels WHERE id = ?1").unwrap();
|
||||
if let Some(user) = user {
|
||||
if let Some(user_id) = user.id {
|
||||
let sql_str = format!("SELECT id,name,user_id FROM pixels WHERE id = ?1 AND user_id = {}", user_id);
|
||||
stmt = conn.prepare(&sql_str).unwrap();
|
||||
}
|
||||
}
|
||||
let pixel = stmt.query_row([id], |row| {
|
||||
Ok(TrackingPixel {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
user_id: row.get(2)?
|
||||
})
|
||||
}).optional();
|
||||
match pixel {
|
||||
Ok(pixel) => {
|
||||
_ = response_sender.send(pixel);
|
||||
Ok(1) // 1 row affected
|
||||
},
|
||||
Err(e) => {
|
||||
_ = response_sender.send(None);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn create_pixel(&self, name: &str, user: &User) -> Result<TrackingPixel, Error> {
|
||||
let id = rand::random::<i64>().abs();
|
||||
// yes I don't check for duplicates. but whatever
|
||||
if user.id.is_none() {
|
||||
return Err(Error::UserNotAuthed());
|
||||
}
|
||||
|
||||
let pixel = TrackingPixel{id, name: name.to_string(), user_id: user.id.unwrap() };
|
||||
self.db_connection_sender.send(DBConnectionMessage::CreatePixel(pixel))?;
|
||||
|
||||
Ok(TrackingPixel { id, name: name.to_string(), user_id: user.id.unwrap() })
|
||||
}
|
||||
|
||||
pub fn delete_pixel(&self, pixel: TrackingPixel, user: Option<&User>) -> Result<(), Error> {
|
||||
if let Some(user) = user {
|
||||
if let Ok(pixel) = self.get_pixel_from_id(pixel.id, Some(user)) {
|
||||
if pixel.is_none() { // pixel is not withing user's pixels
|
||||
return Err(Error::PixelNotFound());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(self.db_connection_sender.send(DBConnectionMessage::DeletePixel(pixel.id))?)
|
||||
}
|
||||
|
||||
pub fn register_hit_with_encoded_pixel(&self, encoded_pixel: &str, ip: &str, user_agent: &str, date: DateTime<Utc>) -> Result<(), Error> {
|
||||
self.register_hit_with_pixel_id(TrackingPixel::url_safe_decode_id(encoded_pixel)?, ip, user_agent, date)
|
||||
}
|
||||
|
||||
|
||||
pub fn register_hit_with_pixel(&self, pixel: &TrackingPixel, ip: &str, user_agent: &str, date: DateTime<Utc>) -> Result<(), Error> {
|
||||
self.register_hit_with_pixel_id(pixel.id, ip, user_agent, date)
|
||||
}
|
||||
|
||||
fn register_hit_with_pixel_id(&self, id: i64, ip: &str, user_agent: &str, date: DateTime<Utc>) -> Result<(), Error> {
|
||||
Ok(self.db_connection_sender.send(
|
||||
DBConnectionMessage::RegisterHit(
|
||||
Hit {
|
||||
pixel_id: id,
|
||||
ip: ip.to_string(),
|
||||
user_agent: user_agent.to_string(),
|
||||
date, id: None
|
||||
})
|
||||
)?
|
||||
)
|
||||
}
|
||||
|
||||
fn pass_hash(password: &str) -> String {
|
||||
hex_digest(Algorithm::SHA256, password.as_bytes())
|
||||
}
|
||||
|
||||
fn auth_user(&self, user: &User, password: &str) -> Result<bool, Error> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
|
||||
let auth_user = User {
|
||||
pass_hash: Self::pass_hash(password),
|
||||
..user.clone()
|
||||
};
|
||||
|
||||
self.db_connection_sender.send(DBConnectionMessage::AuthUser(auth_user, sender))?;
|
||||
|
||||
let auth = receiver.recv()?;
|
||||
|
||||
Ok(auth)
|
||||
}
|
||||
|
||||
fn get_user_from_username(&self, username: &str) -> Result<Option<User>, Error> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
self.db_connection_sender.send(
|
||||
DBConnectionMessage::GetUserFromName(username.to_string(), sender))?;
|
||||
|
||||
Ok(receiver.recv()?)
|
||||
}
|
||||
|
||||
fn get_user_from_id(&self, id: i64) -> Result<Option<User>, Error> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
self.db_connection_sender.send(DBConnectionMessage::GetUserFromId(id, sender))?;
|
||||
|
||||
Ok(receiver.recv()?)
|
||||
}
|
||||
|
||||
pub fn create_user(&self, username: &str, password: &str) -> Result<User, Error>{
|
||||
let user = self.get_user_from_username(username)?;
|
||||
if user.is_some() {
|
||||
return Err(Error::NewUserAlreadyExists());
|
||||
}
|
||||
|
||||
let new_user = User {
|
||||
username: username.to_string(),
|
||||
pass_hash: Self::pass_hash(password),
|
||||
id: None
|
||||
};
|
||||
self.db_connection_sender.send(
|
||||
DBConnectionMessage::CreateUser(new_user))?;
|
||||
|
||||
let user = self.get_user_from_username(username)?;
|
||||
// get user struct that has the id
|
||||
|
||||
if let Some(user) = user {
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(Error::DBError())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_user(&self, user: User) -> Result<(), Error> {
|
||||
let mut id = user.id;
|
||||
if id.is_none() {
|
||||
let db_user = self.get_user_from_username(&user.username)?;
|
||||
|
||||
if let Some(db_user) = db_user {
|
||||
id = db_user.id; // must always be Some because get_user_from_username will always contain an id
|
||||
} else {
|
||||
return Err(Error::InvalidUserData())
|
||||
}
|
||||
}
|
||||
|
||||
self.db_connection_sender.send(DBConnectionMessage::DeleteUser(id.unwrap()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_user(&self, username: &str, password: &str) -> Result<User, Error> {
|
||||
let user = self.get_user_from_username(username)?;
|
||||
if let Some(user) = user {
|
||||
let auth = self.auth_user(&user, password)?;
|
||||
if auth {
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(Error::InvalidCredentials()) // wrong password
|
||||
}
|
||||
} else {
|
||||
Err(Error::InvalidCredentials()) // username does not exist
|
||||
}
|
||||
}
|
||||
// optionally include a user
|
||||
pub fn get_hits_for_pixel(&self, pixel: &TrackingPixel, user: Option<&User>) -> Result<Vec<Hit>, Error> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
self.db_connection_sender.send(DBConnectionMessage::GetHitsForPixel(pixel.clone(), sender, user.cloned()))?;
|
||||
|
||||
let hits = receiver.recv()? ;
|
||||
|
||||
Ok(hits.unwrap_or_default())
|
||||
}
|
||||
|
||||
pub fn get_pixels_for_user(&self, user: &User) -> Result<Vec<TrackingPixel>, Error> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
self.db_connection_sender.send(DBConnectionMessage::GetPixelsForUser(user.clone(), sender))?;
|
||||
|
||||
let pixels = receiver.recv()?;
|
||||
|
||||
|
||||
Ok(pixels.unwrap_or_default())
|
||||
}
|
||||
|
||||
pub fn get_pixel_from_encoded(&self, encoded_id: &str, user: Option<&User>) -> Result<Option<TrackingPixel>, Error> {
|
||||
let id = TrackingPixel::url_safe_decode_id(encoded_id)?;
|
||||
|
||||
self.get_pixel_from_id(id, user)
|
||||
}
|
||||
fn get_pixel_from_id(&self, pixel_id: i64, user: Option<&User>) -> Result<Option<TrackingPixel>, Error> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
self.db_connection_sender.send(DBConnectionMessage::GetPixel(pixel_id, user.cloned(), sender))?;
|
||||
|
||||
Ok(receiver.recv()?)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// stores a version of the pixel manager struct with only a sender, which may be cloned and used in multiple threads
|
||||
#[derive(Debug)]
|
||||
pub struct PixelManagerDelegate {
|
||||
manager: PixelManager
|
||||
}
|
||||
|
||||
impl PixelManagerDelegate {
|
||||
pub fn new(pixel_manager: &PixelManager) -> Self {
|
||||
let internal_pixel_manager = PixelManager {
|
||||
db_connection_sender: pixel_manager.db_connection_sender.clone(),
|
||||
db_connection: None
|
||||
};
|
||||
Self {manager: internal_pixel_manager}
|
||||
}
|
||||
|
||||
pub fn get_manager(&self) -> &PixelManager {
|
||||
&self.manager
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for PixelManagerDelegate {
|
||||
fn clone(&self) -> Self {
|
||||
let internal_pixel_manager = PixelManager {
|
||||
db_connection_sender: self.manager.db_connection_sender.clone(),
|
||||
db_connection: None
|
||||
};
|
||||
Self {manager: internal_pixel_manager}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous" />
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body style="margin-left: 10%; margin-right: 10%;">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
{% block body %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,32 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block body %}
|
||||
|
||||
{% include "navbar.html" %}
|
||||
|
||||
<h1>Pixels for {{ username }}</h1>
|
||||
<form action="/manage/pixel/create" method="post">
|
||||
<div class="form-group">
|
||||
<label for="exampleInputEmail1">Create a new tracking pixel</label>
|
||||
<input class="form-control" name="pixel_name" placeholder="Pixel name" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
</form>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for (name, url, ) in pixels %}
|
||||
<tr>
|
||||
<th scope="row">{{ name|escape }}</th>
|
||||
<td><a class="btn btn-primary" role="button" href="/manage/pixel/{{ url }}">View details</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -0,0 +1,39 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block body %}
|
||||
|
||||
{% include "navbar.html" %}
|
||||
<h1>Pixel {{ pixel_name }}</h1>
|
||||
<div style="display: flex;">
|
||||
<textarea readonly style="flex: 1;">{{ pixel_url }}</textarea>
|
||||
<textarea readonly style="flex: 1;">
|
||||
<img src="{{ pixel_url }}" />
|
||||
</textarea>
|
||||
</div>
|
||||
<form action="/manage/pixel/delete" method="post" onsubmit="return confirm('Are you sure you want to delete this pixel?')">
|
||||
<input type="hidden" name="pixel_id" value="{{ pixel_id }}"/>
|
||||
<button class="btn btn-outline-danger">Delete pixel</button>
|
||||
</form>
|
||||
<h3>Hits for pixel</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">IP</th>
|
||||
<th scope="col">User agent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for (date, ip, user_agent, ) in hits %}
|
||||
<tr>
|
||||
<th scope="row">{{ loop.index }}</th>
|
||||
<td>{{ date|escape }}</td>
|
||||
<td>{{ ip|escape }}</td>
|
||||
<td>{{ user_agent|escape }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -0,0 +1,10 @@
|
|||
<nav class="navbar navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="#">Pixel manager</a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="/manage">Home</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
Loading…
Reference in New Issue