Initial commit
This commit is contained in:
commit
0ddeff0c90
|
@ -0,0 +1 @@
|
||||||
|
*.sqlite
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Dockerfile
|
||||||
|
FROM golang:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN go build -o main .
|
||||||
|
|
||||||
|
CMD ["/app/main"]
|
|
@ -0,0 +1,17 @@
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h2>Your Tracking Pixel Link</h2>
|
||||||
|
<input type="text" id="pixelLink" value="Generated Link Here" readonly>
|
||||||
|
<button onclick="copyLink()">Copy Link</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyLink() {
|
||||||
|
var copyText = document.getElementById("pixelLink");
|
||||||
|
copyText.select();
|
||||||
|
copyText.setSelectionRange(0, 99999); /* For mobile devices */
|
||||||
|
document.execCommand("copy");
|
||||||
|
alert("Copied the link: " + copyText.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Dashboard</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Tracking Pixel Dashboard</h1>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Total Views</th>
|
||||||
|
<th>Unique Views</th>
|
||||||
|
<th>Link</th>
|
||||||
|
</tr>
|
||||||
|
{{range .}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Title}}</td>
|
||||||
|
<td>{{.TotalViews}}</td>
|
||||||
|
<td>{{.UniqueViews}}</td>
|
||||||
|
<td><a href="/p/{{.UUID}}" target="_blank">link</a></td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,66 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var db *sql.DB
|
||||||
|
|
||||||
|
func initDB(filepath string) *sql.DB {
|
||||||
|
db, err := sql.Open("sqlite3", filepath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tables if they do not exist
|
||||||
|
_, err = db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS pixels (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
uuid TEXT UNIQUE,
|
||||||
|
title TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS stats (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
pixel_id INTEGER,
|
||||||
|
view_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ip TEXT,
|
||||||
|
user_agent TEXT,
|
||||||
|
fingerprint TEXT,
|
||||||
|
FOREIGN KEY(pixel_id) REFERENCES pixels(id)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func addStat(pixelID int, ip, userAgent string, fingerprint string) error {
|
||||||
|
statement, err := db.Prepare("INSERT INTO stats (pixel_id, ip, user_agent, fingerprint) VALUES (?, ?, ?, ?)")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = statement.Exec(pixelID, ip, userAgent, fingerprint)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPixelIDFromUUID(uuid string) (int, error) {
|
||||||
|
var id int
|
||||||
|
row := db.QueryRow("SELECT id FROM pixels WHERE uuid = ?", uuid)
|
||||||
|
err := row.Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func savePixel(title, uuid string) error {
|
||||||
|
statement, err := db.Prepare("INSERT INTO pixels (title, uuid) VALUES (?, ?)")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = statement.Exec(title, uuid)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
module trakr
|
||||||
|
|
||||||
|
go 1.21.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.5.0
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.19
|
||||||
|
)
|
|
@ -0,0 +1,4 @@
|
||||||
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
|
@ -0,0 +1,134 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newPixelPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tmpl, err := template.ParseFiles("new.html")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tmpl.Execute(w, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPixelHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "Error parsing form", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
title := r.FormValue("title")
|
||||||
|
|
||||||
|
pixelUUID := uuid.New().String()
|
||||||
|
|
||||||
|
err := savePixel(title, pixelUUID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error saving pixel to the database", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pixelHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
pathParts := strings.Split(r.URL.Path, "/")
|
||||||
|
if len(pathParts) < 3 {
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uuid := pathParts[2]
|
||||||
|
|
||||||
|
// Get the pixel ID from the database
|
||||||
|
pixelID, err := getPixelIDFromUUID(uuid)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error getting pixel ID:", err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := strings.Split(r.RemoteAddr, ":")
|
||||||
|
|
||||||
|
rawFingerprint := fmt.Sprintf(
|
||||||
|
"%s|%s|%s",
|
||||||
|
addr[0],
|
||||||
|
r.Header.Get("User-Agent"),
|
||||||
|
r.Header.Get("Accept-Language"),
|
||||||
|
)
|
||||||
|
hash := sha256.Sum256([]byte(rawFingerprint))
|
||||||
|
fingerprint := string(hash[:])
|
||||||
|
|
||||||
|
err = addStat(pixelID, addr[0], r.UserAgent(), fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error updating stats:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve the pixel image
|
||||||
|
w.Header().Set("Content-Type", "image/gif")
|
||||||
|
w.Write(onePixelGIF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := `
|
||||||
|
SELECT p.title, p.uuid, COUNT(s.id) AS total_views, COUNT(DISTINCT s.ip) AS unique_views
|
||||||
|
FROM pixels p
|
||||||
|
LEFT JOIN stats s ON p.id = s.pixel_id
|
||||||
|
GROUP BY p.id;`
|
||||||
|
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error fetching data", http.StatusInternalServerError)
|
||||||
|
log.Println("Error fetching data:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var stats []PixelStats
|
||||||
|
for rows.Next() {
|
||||||
|
var s PixelStats
|
||||||
|
if err := rows.Scan(&s.Title, &s.UUID, &s.TotalViews, &s.UniqueViews); err != nil {
|
||||||
|
http.Error(w, "Error reading data", http.StatusInternalServerError)
|
||||||
|
log.Println("Error reading data:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stats = append(stats, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle any errors encountered during iteration
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
http.Error(w, "Error iterating data", http.StatusInternalServerError)
|
||||||
|
log.Println("Error iterating data:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the template
|
||||||
|
tmpl, err := template.ParseFiles("dashboard.html")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error loading template", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tmpl.Execute(w, stats)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error executing template", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var onePixelGIF = []byte{
|
||||||
|
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00,
|
||||||
|
0x01, 0x00, 0x80, 0xff, 0x00, 0xff, 0xff, 0xff,
|
||||||
|
0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44,
|
||||||
|
0x01, 0x00, 0x3b,
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db = initDB("data.sqlite")
|
||||||
|
|
||||||
|
http.HandleFunc("/new", newPixelPageHandler)
|
||||||
|
http.HandleFunc("/create-pixel", createPixelHandler)
|
||||||
|
http.HandleFunc("/p/", pixelHandler)
|
||||||
|
http.HandleFunc("/dashboard", dashboardHandler)
|
||||||
|
|
||||||
|
addr := ":8080"
|
||||||
|
|
||||||
|
fmt.Printf("Server is up and running on http://localhost%s\n", addr)
|
||||||
|
|
||||||
|
log.Fatal(http.ListenAndServe(addr, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
type PixelStats struct {
|
||||||
|
Title string
|
||||||
|
TotalViews int
|
||||||
|
UniqueViews int
|
||||||
|
UUID string
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Create New Tracking Pixel</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 300px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h2>Create New Tracking Pixel</h2>
|
||||||
|
<form action="/create-pixel" method="post">
|
||||||
|
<input type="text" name="title" placeholder="Enter pixel title" required>
|
||||||
|
<button type="submit">Create Pixel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
Reference in New Issue