Initial commit

This commit is contained in:
willem640 2023-04-22 22:30:58 +00:00
commit f6797049af
13 changed files with 2558 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1640
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
Cargo.toml Normal file
View File

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

11
TODO Normal file
View File

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

11
assets/hello.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
</head>
<body>
<h1>
Hallo
</h1>
</body>
</html>

BIN
assets/white_pixel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

101
src/db.rs Normal file
View File

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

201
src/main.rs Normal file
View File

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

482
src/pixel.rs Normal file
View File

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

13
templates/base.html Normal file
View File

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

32
templates/index.html Normal file
View File

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

View File

@ -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;">
&lt;img src="{{ pixel_url }}" /&gt;
</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 %}

10
templates/navbar.html Normal file
View File

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