From 5b92454760a8af14bd1031e72024946f868d1de6 Mon Sep 17 00:00:00 2001 From: Parker Date: Mon, 24 Jun 2024 16:24:09 -0500 Subject: Major overhaul + Bare bones web UI --- app/check_api_key.py | 22 ----- app/db.py | 37 -------- app/func/generate_api_key.py | 23 ----- app/func/link/delete.py | 20 ----- app/func/link/delrecords.py | 20 ----- app/func/link/records.py | 23 ----- app/func/link/renew.py | 23 ----- app/func/log.py | 72 ---------------- app/func/newlink.py | 30 ------- app/func/remove_old_data.py | 24 ------ app/linklogger.py | 9 -- app/main.py | 162 ++++++++++++++++++++++++++++++++++ app/routes.py | 201 ------------------------------------------- app/templates/dashboard.html | 47 ++++++++++ app/templates/login.html | 111 ++++++++++++++++++++++++ app/templates/signup.html | 111 ++++++++++++++++++++++++ app/util/log.py | 65 ++++++++++++++ 17 files changed, 496 insertions(+), 504 deletions(-) delete mode 100644 app/check_api_key.py delete mode 100644 app/db.py delete mode 100644 app/func/generate_api_key.py delete mode 100644 app/func/link/delete.py delete mode 100644 app/func/link/delrecords.py delete mode 100644 app/func/link/records.py delete mode 100644 app/func/link/renew.py delete mode 100644 app/func/log.py delete mode 100644 app/func/newlink.py delete mode 100644 app/func/remove_old_data.py delete mode 100644 app/linklogger.py create mode 100644 app/main.py delete mode 100644 app/routes.py create mode 100644 app/templates/dashboard.html create mode 100644 app/templates/login.html create mode 100644 app/templates/signup.html create mode 100644 app/util/log.py (limited to 'app') diff --git a/app/check_api_key.py b/app/check_api_key.py deleted file mode 100644 index d8b92f4..0000000 --- a/app/check_api_key.py +++ /dev/null @@ -1,22 +0,0 @@ -import fastapi -from fastapi import Security, HTTPException -from fastapi.security import APIKeyHeader -import sqlalchemy - -from db import engine - -""" -Make sure the provided API key is valid -""" -api_key_header = APIKeyHeader(name="X-API-Key") - -def check_api_key(api_key_header: str = Security(api_key_header)) -> str: - with engine.begin() as conn: - response = conn.execute(sqlalchemy.text("SELECT api_key FROM keys WHERE api_key = :api_key"), {'api_key': api_key_header}).fetchone() - if response: - return response[0] - else: - raise HTTPException( - status_code=fastapi.status.HTTP_401_UNAUTHORIZED, - detail="Invalid or missing API key" - ) diff --git a/app/db.py b/app/db.py deleted file mode 100644 index 1750472..0000000 --- a/app/db.py +++ /dev/null @@ -1,37 +0,0 @@ -import sqlalchemy -import os - -try: - os.mkdir('data') -except FileExistsError: - pass -engine = sqlalchemy.create_engine('sqlite:///data/data.db') - - -def init_db(): - with engine.begin() as conn: - conn.execute(sqlalchemy.text( - ''' - CREATE TABLE IF NOT EXISTS keys ( - api_key, PRIMARY KEY (api_key) - ) - ''' - )) - conn.execute(sqlalchemy.text( - ''' - CREATE TABLE IF NOT EXISTS links ( - owner, link, redirect_link, expire_date, - FOREIGN KEY (owner) REFERENCES keys(api_key), PRIMARY KEY (link) - ) - ''' - )) - conn.execute(sqlalchemy.text( - ''' - CREATE TABLE IF NOT EXISTS records ( - owner, link, timestamp, ip, location, browser, os, user_agent, isp, - FOREIGN KEY (owner) REFERENCES links(owner), - FOREIGN KEY (link) REFERENCES links(link)) - ''' - )) - - conn.commit() \ No newline at end of file diff --git a/app/func/generate_api_key.py b/app/func/generate_api_key.py deleted file mode 100644 index a40c96a..0000000 --- a/app/func/generate_api_key.py +++ /dev/null @@ -1,23 +0,0 @@ -import sqlalchemy -from sqlalchemy import exc -import random -import string - -from db import engine - -""" -Generate and return a randomized API key string for the user -Keys are composed of 20 uppercase ASCII characters -""" -def generate_api_key(): - with engine.begin() as conn: - while True: - try: - api_key_string = ''.join(random.choices(string.ascii_uppercase, k=20)) - conn.execute(sqlalchemy.text('INSERT INTO keys(api_key) VALUES(:api_key)'), [{'api_key': api_key_string}]) - conn.commit() - break - except exc.IntegrityError: - continue - - return api_key_string \ No newline at end of file diff --git a/app/func/link/delete.py b/app/func/link/delete.py deleted file mode 100644 index 97b696e..0000000 --- a/app/func/link/delete.py +++ /dev/null @@ -1,20 +0,0 @@ -import sqlalchemy - -from db import engine - -""" -Delete the specified link from the users associated links -""" -def delete_link(link, owner): - with engine.begin() as conn: - try: - link_owner = conn.execute(sqlalchemy.text('SELECT owner FROM links WHERE link = :link'), [{'link': link}]).fetchone()[0] - except TypeError: - return 404 - - if owner == link_owner: - with engine.begin() as conn: - conn.execute(sqlalchemy.text('DELETE FROM links WHERE link = :link'), [{'link': link}]) - return link - else: - return 401 \ No newline at end of file diff --git a/app/func/link/delrecords.py b/app/func/link/delrecords.py deleted file mode 100644 index d82bfa5..0000000 --- a/app/func/link/delrecords.py +++ /dev/null @@ -1,20 +0,0 @@ -import sqlalchemy - -from db import engine - -""" -Delete all of the IP log records that are associated with a specific link -""" -def delete_link_records(link, owner): - with engine.begin() as conn: - try: - link_owner = conn.execute(sqlalchemy.text('SELECT owner FROM links WHERE link = :link'), [{'link': link}]).fetchone()[0] - except TypeError: - return 404 - - if owner == link_owner: - with engine.begin() as conn: - conn.execute(sqlalchemy.text('DELETE FROM records WHERE link = :link'), [{'link': link}]) - return link - else: - return 401 \ No newline at end of file diff --git a/app/func/link/records.py b/app/func/link/records.py deleted file mode 100644 index 56bb6d2..0000000 --- a/app/func/link/records.py +++ /dev/null @@ -1,23 +0,0 @@ -import sqlalchemy - -from db import engine - -""" -Retrieve all records associated with a specific link -""" -def get_link_records(link, owner): - with engine.begin() as conn: - try: - link_owner = conn.execute(sqlalchemy.text('SELECT owner FROM links WHERE link = :link'), [{'link': link}]).fetchone()[0] - except TypeError: - return 404 - - if owner == link_owner: - with engine.begin() as conn: - records = conn.execute(sqlalchemy.text('SELECT timestamp, ip, location, browser, os, user_agent, isp FROM records WHERE owner = :owner and link = :link'), [{'owner': owner, 'link': link}]).fetchall() - if not records: - return 204 - else: - return 401 - - return records \ No newline at end of file diff --git a/app/func/link/renew.py b/app/func/link/renew.py deleted file mode 100644 index bcf0550..0000000 --- a/app/func/link/renew.py +++ /dev/null @@ -1,23 +0,0 @@ -import sqlalchemy -import datetime - -from db import engine - -""" -Renew a specified link so that the user can continue logging through that URL -Adds 7 days from the current date -""" -def renew_link(link, owner): - with engine.begin() as conn: - try: - link_owner = conn.execute(sqlalchemy.text('SELECT owner FROM links WHERE link = :link'), [{'link': link}]).fetchone()[0] - except TypeError: - return 404 - - if owner == link_owner: - with engine.begin() as conn: - expire_date = datetime.datetime.date(datetime.datetime.now()) + datetime.timedelta(days=7) - conn.execute(sqlalchemy.text('UPDATE links SET expire_date = :expire_date WHERE link = :link'), [{'expire_date': expire_date, 'link': link}]) - return link, expire_date - else: - return 401 \ No newline at end of file diff --git a/app/func/log.py b/app/func/log.py deleted file mode 100644 index da6594a..0000000 --- a/app/func/log.py +++ /dev/null @@ -1,72 +0,0 @@ -import ip2locationio -import sqlalchemy -import datetime -import validators -from ua_parser import user_agent_parser -from dotenv import load_dotenv -import os -from ip2locationio.ipgeolocation import IP2LocationIOAPIError - -from db import engine - -load_dotenv() -try: - ip_to_location = os.getenv('IP_TO_LOCATION').upper().replace('"', '') - if ip_to_location == 'TRUE': - api_key = os.getenv('API_KEY').replace('"', '') - else: - api_key = "NO_API_KEY" - - base_url = os.getenv('BASE_URL').replace('"', '') -# .env File does not exist - likely a docker run -except AttributeError: - ip_to_location = str(os.environ['IP_TO_LOCATION']).upper().replace('"', '') - if ip_to_location == 'TRUE': - api_key = str(os.environ('API_KEY')).replace('"', '') - else: - api_key = "NO_API_KEY" - - base_url = str(os.environ('BASE_URL')).replace('"', '') - -if not validators.url(base_url): - print(base_url) - print('BASE_URL varaible is malformed.') - exit() - -configuration = ip2locationio.Configuration(api_key) -ipgeolocation = ip2locationio.IPGeolocation(configuration) - -""" -Create a new log record whenever a link is visited -""" -def log(link, ip, user_agent): - with engine.begin() as conn: - try: - redirect_link, owner = conn.execute(sqlalchemy.text('SELECT redirect_link, owner FROM links WHERE link = :link'), [{'link': link}]).fetchone() - except TypeError: - return base_url - - with engine.begin() as conn: - if ip_to_location == 'TRUE': - # Get IP to GEO via IP2Location.io - try: - data = ipgeolocation.lookup(ip) - location = f'{data["country_name"]}, {data["city_name"]}' - isp = data['as'] - # Fatal error, API key is invalid or out of requests, quit - except IP2LocationIOAPIError: - print('Invalid API key or insifficient credit. Change .env file if you do not need IP to location feature.') - location = '-, -' - isp = '-' - else: - location = '-, -' - isp = '-' - - timestamp = datetime.datetime.now() - ua_string = user_agent_parser.Parse(user_agent) - browser = ua_string['user_agent']['family'] - os = f'{ua_string["os"]["family"]} {ua_string["os"]["major"]}' - - conn.execute(sqlalchemy.text('INSERT INTO records (owner, link, timestamp, ip, location, browser, os, user_agent, isp) VALUES (:owner, :link, :timestamp, :ip, :location, :browser, :os, :user_agent, :isp)'), [{'owner': owner, 'link': link, 'timestamp': timestamp, 'ip': ip, 'location': location, 'browser': browser, 'os': os, 'user_agent': user_agent, 'isp': isp}]) - - return redirect_link \ No newline at end of file diff --git a/app/func/newlink.py b/app/func/newlink.py deleted file mode 100644 index 06776e6..0000000 --- a/app/func/newlink.py +++ /dev/null @@ -1,30 +0,0 @@ -import validators -import random -import string -import datetime -import sqlalchemy -from sqlalchemy import exc - -from db import engine - -""" -Generate and return a new randomized link that is connected to the user -Links are composed of 5 uppercase ASCII characters + numbers -""" -def generate_link(redirect_link, owner): - if not validators.url(redirect_link): - return 422 - - with engine.begin() as conn: - choices = string.ascii_uppercase + '1234567890' - while True: - try: - link = ''.join(random.choices(choices, k=5)) - expire_date = datetime.datetime.date(datetime.datetime.now()) + datetime.timedelta(days=7) - conn.execute(sqlalchemy.text('INSERT INTO links(owner, link, redirect_link, expire_date) VALUES (:owner, :link, :redirect_link, :expire_date)'), [{'owner': owner, 'link': link, 'redirect_link': redirect_link, 'expire_date': expire_date}]) - conn.commit() - break - except exc.IntegrityError: - continue - - return link, expire_date diff --git a/app/func/remove_old_data.py b/app/func/remove_old_data.py deleted file mode 100644 index 96d08fa..0000000 --- a/app/func/remove_old_data.py +++ /dev/null @@ -1,24 +0,0 @@ -import sqlalchemy -import datetime - -from db import engine - -""" -Remove all links and associated records when the expire date has passed -""" -def remove_old_data(): - with engine.begin() as conn: - today = datetime.datetime.date(datetime.datetime.now()) - old_links = conn.execute(sqlalchemy.text('SELECT link FROM links WHERE expire_date < :today'), [{'today': today}]) - - delete_links = [] - - for row in old_links: - link = row.link - delete_links.append({'link': link}) - - if delete_links: - with engine.begin() as conn: - conn.execute(sqlalchemy.text('DELETE FROM links WHERE link = :link'), delete_links) - conn.execute(sqlalchemy.text('DELETE FROM records WHERE link = :link'), delete_links) - conn.commit() diff --git a/app/linklogger.py b/app/linklogger.py deleted file mode 100644 index c0d7f15..0000000 --- a/app/linklogger.py +++ /dev/null @@ -1,9 +0,0 @@ -from db import init_db -import uvicorn - -from routes import app - - -if __name__ == '__main__': - init_db() - server = uvicorn.run(app=app, host="0.0.0.0", port=5252) diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..5f6a87b --- /dev/null +++ b/app/main.py @@ -0,0 +1,162 @@ +from flask_login import ( + current_user, + login_user, + login_required, + logout_user, + LoginManager, + UserMixin, +) +from flask import Flask, redirect, render_template, request, url_for, Request +import bcrypt +import os +import string +import random + +from models import User +from database import * +from app.util.log import log + + +class FlaskUser(UserMixin): + pass + + +app = Flask(__name__) +app.config["SECRET_KEY"] = os.urandom(24) + +login_manager = LoginManager() +login_manager.init_app(app) + + +@login_manager.user_loader +def user_loader(username): + user = FlaskUser() + user.id = username + return user + + +""" +Handle login requests from the web UI +""" + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if request.method == "POST": + username = request.form["username"] + password = request.form["password"] + + # Get database session + db = SessionLocal() + + user = db.query(User).filter(User.username == username).first() + db.close() + if not user: + return {"status": "Invalid username or password"} + + if bcrypt.checkpw(password.encode("utf-8"), user.password.encode("utf-8")): + flask_user = FlaskUser() + flask_user.id = username + login_user(flask_user) + return {"status": "success"} + + return {"status": "Invalid username or password"} + return render_template("login.html") + + +""" +Handle signup requests from the web UI +""" + + +@app.route("/signup", methods=["GET", "POST"]) +def signup(): + if request.method == "POST": + username = request.form["username"] + password = request.form["password"] + + # Get database session + db = SessionLocal() + + user = db.query(User).filter(User.username == username).first() + if user: + db.close() + return {"status": "User already exists"} + # Add information to the database + hashed_password = bcrypt.hashpw( + password.encode("utf-8"), bcrypt.gensalt() + ).decode("utf-8") + api_key = "".join(random.choices(string.ascii_letters + string.digits, k=20)) + new_user = User(username=username, password=hashed_password, api_key=api_key) + db.add(new_user) + db.commit() + db.close() + # Log in the newly created user + flask_user = FlaskUser() + flask_user.id = username + login_user(flask_user) + + return {"status": "success"} + return render_template("signup.html") + + +""" +Load the 'dashboard' page for logged in users +""" + + +@app.route("/dashboard", methods=["GET"]) +@login_required +def dashboard(): + # Get database session + db = SessionLocal() + + # Get the API key for the current user + user = db.query(User).filter(User.username == current_user.id).first() + db.close() + api_key = user.api_key + + return render_template("dashboard.html", api_key=api_key) + + +""" +Log users out of their account +""" + + +@app.route("/logout", methods=["GET"]) +@login_required +def logout(): + logout_user() + return redirect(url_for("login")) + + +""" +Log all records for visits to shortened links +""" + + +@app.route("/", methods=["GET"]) +def log_redirect(link): + # If the `link` is more than 5 characters, ignore + if len(link) > 5: + return + + # If the `link` is one of the registered routes, ignore + if link in ["login", "signup", "dashboard", "logout", "api"]: + return + + ip = request.remote_addr + user_agent = request.headers.get("user-agent") + redirect_link = log(link, ip, user_agent) + return redirect(redirect_link) + + +@app.errorhandler(401) +def unauthorized(e): + return redirect(url_for("login")) + + +@app.errorhandler(404) +def not_found(e): + return redirect(url_for("login")) diff --git a/app/routes.py b/app/routes.py deleted file mode 100644 index fa5ab57..0000000 --- a/app/routes.py +++ /dev/null @@ -1,201 +0,0 @@ -import fastapi -from fastapi import Security, HTTPException, Request -from starlette.responses import RedirectResponse -import pydantic -import sqlalchemy - -from db import engine -from check_api_key import check_api_key -from func.generate_api_key import generate_api_key -from func.newlink import generate_link -from func.log import log -from func.link.delete import delete_link -from func.link.renew import renew_link -from func.link.records import get_link_records -from func.link.delrecords import delete_link_records -from func.remove_old_data import remove_old_data - -from apscheduler.schedulers.background import BackgroundScheduler -from contextlib import asynccontextmanager - - -@asynccontextmanager -async def lifespan(app: fastapi.FastAPI): - # Create the scheduler - scheduler = BackgroundScheduler() - scheduler.add_job(remove_old_data, "cron", hour="0", minute="01") - scheduler.start() - yield - -class Newlink(pydantic.BaseModel): - redirect_link: str - -app = fastapi.FastAPI(lifespan=lifespan) - - -@app.post("/api/getapikey") -async def get_api_key(): - """ - Create a new API key - """ - api_key = generate_api_key() - return {"api_key": api_key} - - -@app.post("/api/genlink") -async def newlink(newlink: Newlink, api_key: str = Security(check_api_key)): - """ - Generate a new link that will redirect to the specified URL and log IPs in the middle - """ - data = generate_link(newlink.redirect_link, api_key) - if data == 422: - raise HTTPException( - status_code=fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Malformed redirect link provided" - ) - - return {"link": data[0], "expire_date": data[1]} - - -""" -Return all records associated with an API key, no matter the link -""" -@app.get("/api/records") -async def records(api_key: str = Security(check_api_key)): - """ - Get ALL IP logs records for every link tied to your API key - """ - with engine.begin() as conn: - records = conn.execute(sqlalchemy.text("SELECT timestamp, ip, location, browser, os, user_agent, isp FROM records WHERE owner = :owner"), [{"owner": api_key}]).fetchall() - - if not records: - return {"records": "No records are associated with this API key"} - - response = [] - for timestamp, ip, location, browser, os, user_agent, isp in records: - response.append({"timestamp": timestamp, "ip": ip, "location": location, "browser": browser, "os": os, "user_agent": user_agent, "isp": isp}) - - return response - - -@app.get("/{link}") -def link(link, request: Request): - ip = request.client.host - user_agent = request.headers.get("user-agent") - redirect_link = log(link, ip, user_agent) - return fastapi.responses.RedirectResponse(url=redirect_link) - - -""" -Return all links associated with an API key -""" -@app.get("/api/links") -async def links(api_key: str = Security(check_api_key)): - """ - Retrieve all links that are currently tied to your API key - """ - with engine.begin() as conn: - links = conn.execute(sqlalchemy.text("SELECT link, expire_date FROM links WHERE owner = :owner"), [{"owner": api_key}]).fetchall() - - if not links: - return {"links": "No links are associated with this API key"} - - response = [] - for link, expire_date in links: - response.append({"link": link, "expire_date": expire_date}) - return response - - -@app.post("/api/{link}/delete") -async def link_delete(link: str, api_key: str = Security(check_api_key)): - """ - Delete the specified link and all records associated with it - """ - data = delete_link(link, api_key) - if data == 404: - raise HTTPException( - status_code=fastapi.status.HTTP_404_NOT_FOUND, - detail="Link does not exist" - ) - if data == 401: - raise HTTPException( - status_code=fastapi.status.HTTP_401_UNAUTHORIZED, - detail="Link not associated with given API key" - ) - else: - return {"link": f"The link {data} has been deleted"} - - -@app.post("/api/{link}/renew") -async def link_renew(link: str, api_key: str = Security(check_api_key)): - """ - Renew a specifiec link (adds 7 more days from the current date) - """ - data = renew_link(link, api_key) - if data == 404: - raise HTTPException( - status_code=fastapi.status.HTTP_404_NOT_FOUND, - detail="Link does not exist" - ) - if data == 401: - raise HTTPException( - status_code=fastapi.status.HTTP_401_UNAUTHORIZED, - detail="Link not associated with given API key" - ) - else: - return {"link": f"The link {data[0]} has been renewed and will expire on {data[1]}"} - - -@app.get("/api/{link}/records") -async def link_records(link: str, api_key: str = Security(check_api_key)): - """ - Retrieve all IP log records for the specified link - """ - data = get_link_records(link, api_key) - if data == 404: - raise HTTPException( - status_code=fastapi.status.HTTP_404_NOT_FOUND, - detail="Link does not exist" - ) - if data == 401: - raise HTTPException( - status_code=fastapi.status.HTTP_401_UNAUTHORIZED, - detail="Link not associated with given API key" - ) - if data == 204: - raise HTTPException( - status_code=fastapi.status.HTTP_204_NO_CONTENT, - detail="No records found" - ) - else: - response = [] - for timestamp, ip, location, browser, os, user_agent, isp in data: - response.append({"timestamp": timestamp, "ip": ip, "location": location, "browser": browser, "os": os, "user_agent": user_agent, "isp": isp}) - - return response - - -@app.post("/api/{link}/delrecords") -async def link_delrecords(link: str, api_key: str = Security(check_api_key)): - """ - Delete all IP log records for the specified link - """ - data = delete_link_records(link, api_key) - if data == 404: - raise HTTPException( - status_code=fastapi.status.HTTP_404_NOT_FOUND, - detail="Link does not exist" - ) - if data == 401: - raise HTTPException( - status_code=fastapi.status.HTTP_401_UNAUTHORIZED, - detail="Link not associated with given API key" - ) - else: - return {"link": f"The records for link {data} have been deleted"} - - -# Redirect / -> /docs -@app.get("/", summary="Redirect to the Swagger UI documentation") -async def redirect_to_docs(): - return RedirectResponse(url="/docs") \ No newline at end of file diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html new file mode 100644 index 0000000..554ec03 --- /dev/null +++ b/app/templates/dashboard.html @@ -0,0 +1,47 @@ + + + + + + LinkLogger | Login + + +
+ +

Your API Key: {{ api_key }}

+ +
+ + + + \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..25ce3b6 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,111 @@ + + + + + + LinkLogger | Login + + +
+

Incorrect username/password. Please try again.

+
+ + + +
+
+

Don't have an account? Create one now

+
+ + + + + + \ No newline at end of file diff --git a/app/templates/signup.html b/app/templates/signup.html new file mode 100644 index 0000000..0d2aebd --- /dev/null +++ b/app/templates/signup.html @@ -0,0 +1,111 @@ + + + + + + LinkLogger | Signup + + +
+

User already exists. Please try again

+
+ + + +
+
+

Already have an account? Log in now

+
+ + + + + + \ No newline at end of file diff --git a/app/util/log.py b/app/util/log.py new file mode 100644 index 0000000..e49080f --- /dev/null +++ b/app/util/log.py @@ -0,0 +1,65 @@ +import ip2locationio +import datetime +from ua_parser import user_agent_parser +from ip2locationio.ipgeolocation import IP2LocationIOAPIError + +from database import SessionLocal +from var import LOG, API_KEY, IP_TO_LOCATION +from models import Link, Record + +configuration = ip2locationio.Configuration(API_KEY) +ipgeolocation = ip2locationio.IPGeolocation(configuration) + +""" +Create a new log record whenever a link is visited +""" + + +def log(link, ip, user_agent): + db = SessionLocal() + + # Get the redirect link and owner of the link + redirect_link, owner = ( + db.query(Link.redirect_link, Link.owner).filter(Link.link == link).first() + ) + + if IP_TO_LOCATION == "TRUE": + # Get IP to GEO via IP2Location.io + try: + data = ipgeolocation.lookup(ip) + location = f'{data["country_name"]}, {data["city_name"]}' + isp = data["as"] + # Fatal error, API key is invalid or out of requests, quit + except IP2LocationIOAPIError: + LOG.error( + "Invalid API key or insufficient credits. Change the `config.ini` file if you no longer need the IP2Location API feature." + ) + location = "-, -" + isp = "-" + else: + location = "-, -" + isp = "-" + + timestamp = datetime.datetime.now() + ua_string = user_agent_parser.Parse(user_agent) + browser = ua_string["user_agent"]["family"] + os = f'{ua_string["os"]["family"]} {ua_string["os"]["major"]}' + + # Create the log record and commit it to the database + link_record = Record( + owner=owner, + link=link, + timestamp=timestamp, + ip=ip, + location=location, + browser=browser, + os=os, + user_agent=user_agent, + isp=isp, + ) + db.add(link_record) + db.commit() + db.close() + + # Return the redirect link in order to properly redirect the user + return redirect_link -- cgit v1.2.3-70-g09d2