aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md63
-rw-r--r--api/routes/links_routes.py2
-rw-r--r--api/routes/log_routes.py5
-rw-r--r--app/src/components/Dashboard.tsx12
-rw-r--r--app/src/components/Navbar.tsx2
-rw-r--r--app/src/components/Signup.tsx4
-rw-r--r--app/src/styles/Dashboard.module.css1
-rw-r--r--config.py75
-rw-r--r--database.py17
-rw-r--r--linklogger.py8
10 files changed, 134 insertions, 55 deletions
diff --git a/README.md b/README.md
index d9fd3d6..35cb4a4 100644
--- a/README.md
+++ b/README.md
@@ -1,55 +1,60 @@
-<h1 align="center">
- LinkLogger
-</h1>
+<h1 align="center">LinkLogger</h1>
-<h3 align="center">
- Link shortener and IP logger
-</h3>
+<h3 align="center">Link Shortener and IP Logger</h3>
<p align="center">
<a href="https://github.com/psf/black">
<img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Code Style: Black">
</a>
<a href="https://makeapullrequest.com">
- <img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
+ <img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome">
+ </a>
+ <a href="https://ghcr.io/packetparker/linklogger">
+ <img src="https://img.shields.io/docker/v/packetparker/linklogger?label=Docker" alt="Docker">
</a>
</p>
# Overview
-### Create an account now at [link.pkrm.dev](https://link.pkrm.dev/signup)
-<br>
+### Create an account at [link.pkrm.dev/signup](https://link.pkrm.dev/signup)
-LinkLogger is simple and public API to create redirect links and log IPs. Every visit to a registered short link will log the users IP address, location, user agent, browser, and OS before redirecting them to a specific URL.
+**LinkLogger** is an *extremely* simple and public link shortener and IP logger. Every visit to a registered short link will log the user's IP address, location, user agent, browser, and OS before redirecting them to a specific URL.
-Just like Grabify, but unrestricted and with no real web UI.
+The API is built on **FastAPI**, and the UI is built with **React**.
+*NOTE:* I am NOT a front-end dev, so don't expect much on that. 😅
Feel free to submit an issue for any problems you experience or if you have an idea for a new feature. If you have a fix for anything, feel free to submit a pull request for review.
+**TL;DR:** LinkLogger is like Grabify, but unrestricted and with a rudimentary UI.
+
# API Reference
-View the API reference and try out the endpoints at the [docs page](https://link.pkrm.dev/api/docs)
-# Want to self-host?
+You can view the full **API reference** and try out the endpoints on the [docs page](https://link.pkrm.dev/docs).
+
+# Want to Self-Host?
+## Docker
+
+Docker is the recommended method of hosting LinkLogger. Running on bare metal is recommended for development.
+
+To run LinkLogger on Docker, check out the [docker-compose.yaml](docker-compose.yaml) file.
## Bare metal
-To run LinkLogger on bare metal, follow the steps below.
-*NOTE: For information on each configuration variable, look at the `Configuration` section of this page.*
+If you want to work on the LinkLogger source, or just want to run the project on bare metal, follow the instructions below:
-1. Install Python and Pip
-2. Clone this repository
-3. Install the requirements with `pip install -r requirements.txt`
-4. Run the `linklogger.py` file
-5. Input information into the newly created `config.ini` file.
-6. Re-run the `linklogger.py` file.
+1. Install python3 (preferably >3.10) & pip
+2. Install Node.js
+3. Install Yarn
+4. Install API dependencies (pip install -r requirements.txt)
+5. Run either [linklogger.sh](linklogger.sh) (Linux/MacOS) or [linklogger.bat](linklogger.bat) (Windows)
-## Docker
-To run LinkLogger in Docker, use the [docker-compose.yaml](/docker-compose.yaml) as a template for the contianer.
+*NOTE: Running on bare metal means there is not an NGINX instance to serve the UI and proxy API requests to port 5252.*
+
+## Configuration
-## Config
-Below are all of the configuration variables that are used within LinkLogger.
+Below are all of the configuration variables that are used within the LinkLogger config.yaml file.
-Variable | Description | Requirement
----|---|---
-IP_TO_LOCATION | `BOOLEAN`: Whether or not you want toe IP to Location feature <br> *(requires IP2Location.io account)* | **Required**
-API_KEY | `KEY`: IP2Location.io API Key | **Required** *only if IP_TO_LOCATION is set to True* \ No newline at end of file
+| **Variable** | **Description** | **Requirement** |
+|---------------------|------------------|-----------------|
+| `IP_TO_LOCATION` | `BOOLEAN`: Whether or not you want the IP-to-Location feature. *(requires IP2Location.io account)* | **Required** |
+| `API_KEY` | `API KEY`: IP2Location.io API Key | **Required** (only if `IP_TO_LOCATION` is set to `True`) | \ No newline at end of file
diff --git a/api/routes/links_routes.py b/api/routes/links_routes.py
index 97b4599..c060f3f 100644
--- a/api/routes/links_routes.py
+++ b/api/routes/links_routes.py
@@ -57,7 +57,7 @@ async def create_link(
link=link_path,
owner=current_user.id,
redirect_link=url.url,
- expire_date=datetime.datetime.utcnow()
+ expire_date=datetime.datetime.today()
+ datetime.timedelta(days=30),
)
db.add(new_link)
diff --git a/api/routes/log_routes.py b/api/routes/log_routes.py
index 5875e3b..438fbf6 100644
--- a/api/routes/log_routes.py
+++ b/api/routes/log_routes.py
@@ -47,9 +47,10 @@ async def get_logs_for_link(
Get all of the logs associated with a specific link
- check to make sure the requester is the owner
"""
+ link = link.upper()
link = (
db.query(Link)
- .filter(Link.owner == current_user.id, Link.short == link)
+ .filter(Link.owner == current_user.id, Link.link == link)
.first()
)
if not link:
@@ -58,7 +59,7 @@ async def get_logs_for_link(
)
logs = (
db.query(Log)
- .filter(Log.link_id == link.id)
+ .filter(Log.link == link.link)
.order_by(Log.timestamp.desc())
.all()
)
diff --git a/app/src/components/Dashboard.tsx b/app/src/components/Dashboard.tsx
index d2bca9e..d699efb 100644
--- a/app/src/components/Dashboard.tsx
+++ b/app/src/components/Dashboard.tsx
@@ -175,7 +175,7 @@ function Dashboard() {
{logs.filter((log) => log.link === link.link).length || 0}
</td>
<td>{link.redirect_link}</td>
- <td>{link.expire_date}</td>
+ <td>{new Date(link.expire_date).toLocaleDateString()}</td>
</tr>
{/* Conditionally render logs for this link */}
@@ -199,7 +199,15 @@ function Dashboard() {
.map((log, index, filteredLogs) => (
<tr key={log.id}>
<td>{filteredLogs.length - index}</td>
- <td>{log.timestamp}</td>
+ <td>
+ {new Date(
+ log.timestamp
+ ).toLocaleTimeString() +
+ ', ' +
+ new Date(
+ log.timestamp
+ ).toLocaleDateString()}
+ </td>
<td>{log.ip}</td>
<td>{log.location}</td>
<td>{log.isp}</td>
diff --git a/app/src/components/Navbar.tsx b/app/src/components/Navbar.tsx
index ea49ea8..79efce2 100644
--- a/app/src/components/Navbar.tsx
+++ b/app/src/components/Navbar.tsx
@@ -32,7 +32,7 @@ function Navbar() {
<a className={styles.link}>Login</a>
</Link>
<Link to={'/signup'}>
- <a className={styles.link}>Signup</a>
+ <a className={styles.link}>Create Account</a>
</Link>
</div>
<div className={styles.right}>
diff --git a/app/src/components/Signup.tsx b/app/src/components/Signup.tsx
index c01784e..0822c46 100644
--- a/app/src/components/Signup.tsx
+++ b/app/src/components/Signup.tsx
@@ -6,7 +6,7 @@ import axios from 'axios';
import Navbar from './Navbar';
function Signup() {
- document.title = 'LinkLogger | Signup';
+ document.title = 'LinkLogger | Create Account';
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
@@ -65,7 +65,7 @@ function Signup() {
<>
<Navbar />
<div className={styles.container}>
- <h1>Sign up</h1>
+ <h1>Create Account</h1>
<h2 className={error ? styles.errorVisible : styles.errorHidden}>
{error}
</h2>
diff --git a/app/src/styles/Dashboard.module.css b/app/src/styles/Dashboard.module.css
index bfc852e..fbc1d6d 100644
--- a/app/src/styles/Dashboard.module.css
+++ b/app/src/styles/Dashboard.module.css
@@ -87,6 +87,7 @@ table td {
.logTableRow table td {
background-color: #3b4148;
padding: 10px;
+ font-size: 17px;
}
.logTableRow table tr {
diff --git a/config.py b/config.py
index 9b6f23c..54f10fe 100644
--- a/config.py
+++ b/config.py
@@ -23,6 +23,12 @@ LOG.addHandler(stream)
IP_TO_LOCATION = None
API_KEY = None
+DB_NAME = None
+DB_ENGINE = None
+DB_HOST = None
+DB_PORT = None
+DB_USER = None
+DB_PASSWORD = None
schema = {
"type": "object",
@@ -34,7 +40,26 @@ schema = {
"api_key": {"type": "string"},
},
"required": ["ip_to_location"],
- }
+ },
+ "database": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "default": "linklogger"},
+ "engine": {"type": "string"},
+ "host": {"type": "string"},
+ "port": {"type": "integer"},
+ "user": {"type": "string"},
+ "password": {"type": "string"},
+ },
+ "required": [
+ "name",
+ "engine",
+ "host",
+ "port",
+ "user",
+ "password",
+ ],
+ },
},
"required": ["config"],
}
@@ -50,16 +75,26 @@ def load_config():
try:
with open(file_path, "r") as f:
file_contents = f.read()
- validate_config(file_contents)
+ if not validate_config(file_contents):
+ return False
+ else:
+ return True
except FileNotFoundError:
# Create new config.yaml w/ template
with open(file_path, "w") as f:
f.write(
- """
-config:
+ """config:
ip_to_location: false
- api_key: ''"""
+ api_key: ''
+
+database:
+ engine: 'sqlite'
+ name: ''
+ host: ''
+ port: ''
+ user: ''
+ password: ''"""
)
LOG.critical(
"`config.yaml` was not found, a template has been created."
@@ -67,12 +102,10 @@ config:
)
return False
- return True
-
# Validate the options within config.yaml
def validate_config(file_contents):
- global IP_TO_LOCATION, API_KEY
+ global IP_TO_LOCATION, API_KEY, DB_NAME, DB_ENGINE, DB_HOST, DB_PORT, DB_USER, DB_PASSWORD
config = yaml.safe_load(file_contents)
try:
@@ -88,5 +121,31 @@ def validate_config(file_contents):
if IP_TO_LOCATION:
if not config["config"]["api_key"]:
LOG.error("API_KEY is not set")
+ return False
else:
API_KEY = config["config"]["api_key"]
+
+ #
+ # Set/Validate the DATABASE section of the config.yaml
+ #
+ if "database" in config:
+ if config["database"]["engine"] not in [
+ "sqlite",
+ "mysql",
+ "postgresql",
+ ]:
+ LOG.error(
+ "database_engine must be either 'sqlite', 'mysql', or"
+ " 'postgresql'"
+ )
+ return False
+ else:
+ DB_ENGINE = config["database"]["engine"]
+
+ DB_NAME = config["database"]["name"]
+ DB_HOST = config["database"]["host"]
+ DB_PORT = config["database"]["port"]
+ DB_USER = config["database"]["user"]
+ DB_PASSWORD = config["database"]["password"]
+
+ return True
diff --git a/database.py b/database.py
index 544ee05..0166d28 100644
--- a/database.py
+++ b/database.py
@@ -1,13 +1,18 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
-import os
-# Create 'data' directory at root if it doesn't exist
-if not os.path.exists("data"):
- os.makedirs("data")
+import config
-engine = create_engine("sqlite:///data/data.db")
-SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+if config.DB_ENGINE == "mysql":
+ database_url = f"mysql+pymysql://{config.DB_USER}:{config.DB_PASSWORD}@{config.DB_HOST}:{config.DB_PORT}/{config.DB_NAME}"
+
+elif config.DB_ENGINE == "postgresql":
+ database_url = f"postgresql+psycopg2://{config.DB_USER}:{config.DB_PASSWORD}@{config.DB_HOST}:{config.DB_PORT}/{config.DB_NAME}"
+else:
+ database_url = "sqlite:///data/data.db"
+
+engine = create_engine(database_url)
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
diff --git a/linklogger.py b/linklogger.py
index c456a16..17837b0 100644
--- a/linklogger.py
+++ b/linklogger.py
@@ -1,12 +1,12 @@
import uvicorn
import config
-from api.main import app
-from database import Base, engine
-Base.metadata.create_all(bind=engine)
-
if __name__ == "__main__":
if config.load_config():
+ from api.main import app
+ from database import Base, engine
+
+ Base.metadata.create_all(bind=engine)
uvicorn.run(app, port=5252)