diff options
-rw-r--r-- | README.md | 63 | ||||
-rw-r--r-- | api/routes/links_routes.py | 2 | ||||
-rw-r--r-- | api/routes/log_routes.py | 5 | ||||
-rw-r--r-- | app/src/components/Dashboard.tsx | 12 | ||||
-rw-r--r-- | app/src/components/Navbar.tsx | 2 | ||||
-rw-r--r-- | app/src/components/Signup.tsx | 4 | ||||
-rw-r--r-- | app/src/styles/Dashboard.module.css | 1 | ||||
-rw-r--r-- | config.py | 75 | ||||
-rw-r--r-- | database.py | 17 | ||||
-rw-r--r-- | linklogger.py | 8 |
10 files changed, 134 insertions, 55 deletions
@@ -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 { @@ -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) |