mirror of
https://github.com/JonasunderscoreJones/analytics.jonasjones.dev.git
synced 2025-10-22 17:09:18 +02:00
initial code
This commit is contained in:
parent
57a91f2e76
commit
1715184876
12 changed files with 1998 additions and 2 deletions
13
.editorconfig
Normal file
13
.editorconfig
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -128,3 +128,6 @@ dist
|
||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
|
||||||
|
.dev.vars
|
||||||
|
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"printWidth": 140,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"useTabs": true
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
[{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/69","countrycode":"US","iphash":"hashed_ip"},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/69","countrycode":{},"iphash":"hashed_ip"},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/69","countrycode":{},"iphash":{}},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/420","countrycode":{},"iphash":{}},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/420","countrycode":{},"iphash":{}},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/420","countrycode":{},"iphash":{}},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/420","countrycode":{},"iphash":{}},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/420","countrycode":{},"iphash":{}},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/420","countrycode":{},"iphash":{}},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/420","countrycode":{},"iphash":{}},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/420","countrycode":"US","iphash":"a10f575be500ba832cd5ea7a75fbcbc5d12542541e6cac7e8a7994930bf990f9"},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/421","countrycode":"US","iphash":"a10f575be500ba832cd5ea7a75fbcbc5d12542541e6cac7e8a7994930bf990f9"},{"timestamp":1628884800,"domain":"example.com","method":"GET","path":"/get/421","ipcountry":"US"}]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,2 +1,2 @@
|
||||||
# analytics.jonasjones.dev
|
# aka-worker
|
||||||
analytics worker for jonasjones ecosystem
|
Cloudflare aka link worker for aka.jonasjones.dev
|
||||||
|
|
1658
package-lock.json
generated
Normal file
1658
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
12
package.json
Normal file
12
package.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "analytics-jonasjones-dev",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"deploy": "wrangler deploy",
|
||||||
|
"start": "wrangler dev"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"wrangler": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
244
src/index.js
Normal file
244
src/index.js
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
addEventListener('fetch', event => {
|
||||||
|
event.respondWith(handleRequest(event.request))
|
||||||
|
})
|
||||||
|
|
||||||
|
const AUTH_KEY = AUTH_KEY_SECRET
|
||||||
|
const FILE_KEY = 'analytics/requests.json'
|
||||||
|
const ALLOWED_ORIGINS = [
|
||||||
|
'https://jonasjones.dev',
|
||||||
|
'https://www.jonasjones.dev',
|
||||||
|
'https://blog.jonasjones.dev',
|
||||||
|
'https://docs.jonasjones.dev',
|
||||||
|
'https://analytics.jonasjones.dev',
|
||||||
|
'https://wiki.jonasjones.dev',
|
||||||
|
'https://kcomebacks.jonasjones.dev',
|
||||||
|
'https://jonasjonesstudios.com',
|
||||||
|
'https://lastlovedsyncify.jonasjones.dev',
|
||||||
|
'https://syncify.jonasjones.dev'
|
||||||
|
]
|
||||||
|
|
||||||
|
async function handleRequest(request) {
|
||||||
|
const url = new URL(request.url)
|
||||||
|
|
||||||
|
if (url.pathname === '/requests/record' && request.method === 'POST') {
|
||||||
|
return handleRecordRequest(request)
|
||||||
|
} else if (url.pathname === '/requests/record/ipunknown' && request.method === 'POST') {
|
||||||
|
return handleRecordIpRequest(request)
|
||||||
|
} else if (url.pathname === '/requests/get/count') {
|
||||||
|
return handleGetCountRequest(request)
|
||||||
|
} else if (url.pathname.startsWith('/requests/get')) {
|
||||||
|
return handleGetRequest(url)
|
||||||
|
} else if (request.method === 'OPTIONS') {
|
||||||
|
return handleOptions(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Not Found', { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOptions(request) {
|
||||||
|
const origin = request.headers.get('Origin');
|
||||||
|
const isAllowedOrigin = ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('localhost');
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
'Access-Control-Max-Age': '86400', // Cache the preflight response for 1 day
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(null, {
|
||||||
|
headers: headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function handleRecordRequest(request) {
|
||||||
|
const { headers } = request
|
||||||
|
const authKey = headers.get('Authorization')
|
||||||
|
|
||||||
|
if (authKey !== AUTH_KEY) {
|
||||||
|
return new Response('Unauthorized', { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { timestamp, domain, method, path, ipcountry } = await request.json()
|
||||||
|
|
||||||
|
if (!timestamp || !domain || !method || !path || !ipcountry) {
|
||||||
|
return new Response('Bad Request: Missing required fields', { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = { timestamp, domain, method, path, ipcountry }
|
||||||
|
|
||||||
|
// Retrieve existing records from R2
|
||||||
|
let records = await getRecordsFromR2()
|
||||||
|
if (!records) {
|
||||||
|
records = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new record to the list
|
||||||
|
records.push(record)
|
||||||
|
|
||||||
|
// Store the updated list back to R2
|
||||||
|
await uploadRecordsToR2(records)
|
||||||
|
|
||||||
|
return new Response('Recorded', { status: 200 })
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error recording request:', error)
|
||||||
|
return new Response('Bad Request: Invalid JSON', { status: 400 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRecordIpRequest(request) {
|
||||||
|
const { headers } = request
|
||||||
|
const authKey = headers.get('Authorization')
|
||||||
|
|
||||||
|
if (authKey !== AUTH_KEY) {
|
||||||
|
return new Response('Unauthorized', { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { timestamp, domain, method, path } = await request.json()
|
||||||
|
|
||||||
|
if (!timestamp || !domain || !method || !path) {
|
||||||
|
return new Response('Bad Request: Missing required fields', { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipcountry = request.cf.country
|
||||||
|
|
||||||
|
const record = { timestamp, domain, method, path, ipcountry }
|
||||||
|
|
||||||
|
// Retrieve existing records from R2
|
||||||
|
let records = await getRecordsFromR2()
|
||||||
|
if (!records) {
|
||||||
|
records = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new record to the list
|
||||||
|
records.push(record)
|
||||||
|
|
||||||
|
// Store the updated list back to R2
|
||||||
|
await uploadRecordsToR2(records)
|
||||||
|
|
||||||
|
const origin = request.headers.get('Origin');
|
||||||
|
const isAllowedOrigin = ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('localhost');
|
||||||
|
|
||||||
|
return new Response('Recorded', { status: 200 }, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error recording request:', error)
|
||||||
|
return new Response('Bad Request: Invalid JSON', { status: 400 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetCountRequest(request) {
|
||||||
|
const origin = request.headers.get('Origin');
|
||||||
|
const isAllowedOrigin = ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('localhost');
|
||||||
|
|
||||||
|
const records = await getRecordsFromR2() || []
|
||||||
|
const count = records.length
|
||||||
|
return new Response(JSON.stringify({ count }), {
|
||||||
|
headers: { 'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetRequest(url) {
|
||||||
|
const params = new URLSearchParams(url.search)
|
||||||
|
|
||||||
|
const start = parseInt(params.get('start')) || 0
|
||||||
|
const end = parseInt(params.get('end')) || Number.MAX_SAFE_INTEGER
|
||||||
|
const count = Math.min(parseInt(params.get('count')) || 100, 100)
|
||||||
|
const offset = parseInt(params.get('offset')) || 0
|
||||||
|
const domain = params.get('domain')
|
||||||
|
const method = params.get('method')
|
||||||
|
const path = params.get('path')
|
||||||
|
const ipcountry = params.get('ipcountry')
|
||||||
|
|
||||||
|
let records = await getRecordsFromR2() || []
|
||||||
|
|
||||||
|
let filteredRecords = records
|
||||||
|
.filter(record => {
|
||||||
|
return (
|
||||||
|
record.timestamp >= start &&
|
||||||
|
record.timestamp <= end &&
|
||||||
|
(!domain || record.domain === domain) &&
|
||||||
|
(!method || record.method === method) &&
|
||||||
|
(!path || record.path === path) &&
|
||||||
|
(!ipcountry || record.ipcountry === ipcountry)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.slice(offset, offset + count)
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(filteredRecords), {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRecordsFromR2() {
|
||||||
|
try {
|
||||||
|
const object = await CDN_BUCKET.get(FILE_KEY)
|
||||||
|
if (object === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const text = await object.text()
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching records:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadRecordsToR2(records) {
|
||||||
|
try {
|
||||||
|
const json = JSON.stringify(records)
|
||||||
|
await CDN_BUCKET.put(FILE_KEY, json)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading records:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCountryCode(ip) {
|
||||||
|
const response = await fetch(`https://ipinfo.io/${ip}/country?token=${IPINFO_TOKEN}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the response is plain text, use response.text()
|
||||||
|
const countryCode = await response.text();
|
||||||
|
|
||||||
|
// Trim any whitespace (like newline characters) from the response
|
||||||
|
const trimmedCountryCode = countryCode.trim();
|
||||||
|
|
||||||
|
// Check if the response is a valid country code
|
||||||
|
if (!trimmedCountryCode) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedCountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hashString(input) {
|
||||||
|
// Encode the input string as a Uint8Array (UTF-8)
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(input);
|
||||||
|
|
||||||
|
// Hash the data using SHA-256
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
|
||||||
|
// Convert the hash to a hexadecimal string
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
|
||||||
|
return hashHex;
|
||||||
|
}
|
59
wrangler.toml
Normal file
59
wrangler.toml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
name = "analytics-jonasjones-dev"
|
||||||
|
main = "src/index.js"
|
||||||
|
compatibility_date = "2023-09-04"
|
||||||
|
type = "javascript"
|
||||||
|
|
||||||
|
account_id = "d9e259f328167af6c866c4e624f12c0d"
|
||||||
|
workers_dev = true
|
||||||
|
|
||||||
|
[[r2_buckets]]
|
||||||
|
binding = "CDN_BUCKET"
|
||||||
|
bucket_name = "cdn"
|
||||||
|
|
||||||
|
# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
|
||||||
|
# Note: Use secrets to store sensitive data.
|
||||||
|
# Docs: https://developers.cloudflare.com/workers/platform/environment-variables
|
||||||
|
# [vars]
|
||||||
|
# MY_VARIABLE = "production_value"
|
||||||
|
|
||||||
|
# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.
|
||||||
|
# Docs: https://developers.cloudflare.com/workers/runtime-apis/kv
|
||||||
|
# [[kv_namespaces]]
|
||||||
|
# binding = "MY_KV_NAMESPACE"
|
||||||
|
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
||||||
|
# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files.
|
||||||
|
# Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/
|
||||||
|
# [[r2_buckets]]
|
||||||
|
# binding = "MY_BUCKET"
|
||||||
|
# bucket_name = "my-bucket"
|
||||||
|
|
||||||
|
# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer.
|
||||||
|
# Docs: https://developers.cloudflare.com/queues/get-started
|
||||||
|
# [[queues.producers]]
|
||||||
|
# binding = "MY_QUEUE"
|
||||||
|
# queue = "my-queue"
|
||||||
|
|
||||||
|
# Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them.
|
||||||
|
# Docs: https://developers.cloudflare.com/queues/get-started
|
||||||
|
# [[queues.consumers]]
|
||||||
|
# queue = "my-queue"
|
||||||
|
|
||||||
|
# Bind another Worker service. Use this binding to call another Worker without network overhead.
|
||||||
|
# Docs: https://developers.cloudflare.com/workers/platform/services
|
||||||
|
# [[services]]
|
||||||
|
# binding = "MY_SERVICE"
|
||||||
|
# service = "/api/*"
|
||||||
|
|
||||||
|
# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model.
|
||||||
|
# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps.
|
||||||
|
# Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects
|
||||||
|
# [[durable_objects.bindings]]
|
||||||
|
# name = "MY_DURABLE_OBJECT"
|
||||||
|
# class_name = "MyDurableObject"
|
||||||
|
|
||||||
|
# Durable Object migrations.
|
||||||
|
# Docs: https://developers.cloudflare.com/workers/learning/using-durable-objects#configure-durable-object-classes-with-migrations
|
||||||
|
# [[migrations]]
|
||||||
|
# tag = "v1"
|
||||||
|
# new_classes = ["MyDurableObject"]
|
Loading…
Add table
Add a link
Reference in a new issue