├── .github └── workflows │ ├── develop-docker-build.yml │ └── docker-build.yml ├── .gitignore ├── LICENSE ├── README.md ├── VERSION ├── app ├── Dockerfile ├── app.py ├── db.py ├── docker-compose.yml ├── metrics.py ├── requirements.txt ├── routes.py ├── static │ └── styles.css ├── templates │ ├── admin.html │ ├── index.html │ └── static │ │ └── styles.css └── utils.py └── images ├── admin.png └── generate.png /.github/workflows/develop-docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Development Docker Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - development 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | branches: 11 | - development 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v2 23 | with: 24 | platforms: arm64, amd64 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v2 28 | 29 | - name: Log in to Docker Hub 30 | uses: docker/login-action@v2 31 | with: 32 | username: ${{ secrets.DOCKER_USERNAME }} 33 | password: ${{ secrets.DOCKER_PASSWORD }} 34 | 35 | - name: Extract version from VERSION file 36 | id: version 37 | run: | 38 | VERSION=$(cat VERSION) 39 | echo "VERSION=${VERSION}" >> $GITHUB_ENV 40 | 41 | 42 | - name: Build and push Docker image 43 | uses: docker/build-push-action@v5 44 | with: 45 | context: ./app 46 | platforms: linux/amd64,linux/arm64 47 | push: true 48 | tags: | 49 | schech1/applinkr:develop-${{ env.VERSION }} 50 | schech1/applinkr:develop-latest 51 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v2 23 | with: 24 | platforms: arm64, amd64 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v2 28 | 29 | - name: Log in to Docker Hub 30 | uses: docker/login-action@v2 31 | with: 32 | username: ${{ secrets.DOCKER_USERNAME }} 33 | password: ${{ secrets.DOCKER_PASSWORD }} 34 | 35 | - name: Extract version from VERSION file 36 | id: version 37 | run: | 38 | VERSION=$(cat VERSION) 39 | echo "VERSION=${VERSION}" >> $GITHUB_ENV 40 | 41 | 42 | - name: Build and push Docker image 43 | uses: docker/build-push-action@v5 44 | with: 45 | context: ./app 46 | platforms: linux/amd64,linux/arm64 47 | push: true 48 | tags: | 49 | schech1/applinkr:${{ env.VERSION }} 50 | schech1/applinkr:latest 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | *.db 3 | *.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 schech1 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppLinkr 2 | 3 | AppLinkr is a simple web application that generates a QR code and forwards users to the appropriate App Store or Google Play Store based on their device type. With AppLinkr, you can easily share your mobile application links and track user interactions. 4 | 5 | ## Demo 6 | 7 | Test out **AppLinkr** at [https://demo.applinkr.one](https://demo.applinkr.one) 8 | 9 | Admin password: `applinkr-demo` 10 | 11 | The database will reset every 24 hours. 12 | 13 | ## Features 14 | 15 | - **QR Code Generation**: Generate a unique QR code for your app with a simple interface. 16 | - **Device Detection**: Automatically redirects users to the appropriate app store based on their device (iOS or Android). 17 | - **Tracking**: Monitor user interactions with each QR code, including access count and device type. 18 | - **Admin Panel**: View statistics, manage QR codes, and download the database backup. 19 | 20 | 21 | ## Roadmap 22 | 23 | - Enhance stats with graphs 24 | - Improve UI design 25 | - Individualize QR-codes with frames and embedded images 26 | 27 | **Hint:** *If you are interested in contributing to AppLinkr, feel free to do so. It would help me a lot.* 28 | 29 | ## Installation (via docker-compose) 30 | 31 | ```yaml 32 | version: '3.8' 33 | 34 | services: 35 | applinkr: 36 | image: schech1/applinkr:latest 37 | ports: 38 | - "5001:5001" 39 | environment: 40 | PASSWORD: admin 41 | SERVER_URL: "https://qr.domain.com" 42 | volumes: 43 | - ./db:/app/db 44 | ``` 45 | 46 | ## Usage 47 | Navigate to your domain, to open the QR-Code-Generator. 48 | 49 | Navigate to `/admin` to access the admin panel. Login with your defined password. 50 | 51 | ## Contributing 52 | 53 | ### Help Wanted 54 | I'm looking for someone with UI/UX-Experience to improve the design of the app. 55 | If you are interested to contribute, let me know. 56 | 57 | ## License 58 | 59 | This project is licensed under the MIT License. See the LICENSE file for more details. 60 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v0.1.0 2 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | WORKDIR /app 3 | COPY . /app 4 | RUN pip install --no-cache-dir -r requirements.txt 5 | EXPOSE 5001 6 | CMD ["python", "-u", "app.py"] 7 | -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | import os 3 | from db import close_db 4 | from routes import setup_routes 5 | 6 | app = Flask(__name__) 7 | app.secret_key = os.urandom(24) 8 | 9 | # Database setup 10 | # Define the default database location inside the 'db' folder 11 | DATABASE_FOLDER = "db" 12 | DATABASE_PATH = f"{DATABASE_FOLDER}/database.db" 13 | 14 | # Ensure the 'db' folder exists 15 | if not os.path.exists(DATABASE_FOLDER): 16 | os.makedirs(DATABASE_FOLDER) 17 | 18 | # GET ENVS 19 | SERVER_URL = os.getenv("SERVER_URL") 20 | PASSWORD = os.getenv("PASSWORD") 21 | 22 | # Register the database teardown function 23 | @app.teardown_appcontext 24 | def teardown(exception): 25 | close_db(exception) 26 | 27 | # Set up routes 28 | setup_routes(app, SERVER_URL, PASSWORD) 29 | 30 | if __name__ == '__main__': 31 | app.run(host='0.0.0.0', port=5001, debug=True) 32 | -------------------------------------------------------------------------------- /app/db.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from flask import g 3 | 4 | 5 | DATABASE_FOLDER = "db" 6 | DATABASE_PATH = f"{DATABASE_FOLDER}/database.db" 7 | 8 | def get_db(): 9 | """Get a database connection.""" 10 | if 'db' not in g: 11 | g.db = sqlite3.connect(DATABASE_PATH) 12 | 13 | # Create qr_codes table with qr_image allowing NULL values 14 | g.db.execute('''CREATE TABLE IF NOT EXISTS qr_codes 15 | (id TEXT PRIMARY KEY, 16 | title TEXT NOT NULL, 17 | content TEXT, 18 | app_store_url TEXT NOT NULL, 19 | play_store_url TEXT NOT NULL, 20 | qr_image BLOB)''') 21 | 22 | g.db.execute('''CREATE TABLE IF NOT EXISTS qr_code_tracking 23 | (id INTEGER PRIMARY KEY AUTOINCREMENT, 24 | qr_code_id TEXT NOT NULL, 25 | device_type TEXT NOT NULL, 26 | ip_address TEXT NOT NULL, 27 | access_time DATETIME DEFAULT CURRENT_TIMESTAMP, 28 | region TEXT, 29 | browser TEXT, 30 | os TEXT, 31 | language TEXT, 32 | referrer TEXT, 33 | FOREIGN KEY (qr_code_id) REFERENCES qr_codes(id))''') 34 | return g.db 35 | 36 | def close_db(exception): 37 | """Close the database connection.""" 38 | db = g.pop('db', None) 39 | if db is not None: 40 | db.close() 41 | 42 | def delete_qr_code_by_id(qr_code_id): 43 | """Delete a QR code by its ID.""" 44 | db = get_db() 45 | db.execute('DELETE FROM qr_codes WHERE id = ?', (qr_code_id,)) 46 | db.execute('DELETE FROM qr_code_tracking WHERE qr_code_id = ?', (qr_code_id,)) 47 | db.commit() 48 | -------------------------------------------------------------------------------- /app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | applinkr: 5 | image: schech1/applinkr:latest 6 | ports: 7 | - "5001:5001" 8 | environment: 9 | PASSWORD: admin 10 | SERVER_URL: "https://qr.domain.com" 11 | volumes: 12 | - ./db:/app/db 13 | 14 | -------------------------------------------------------------------------------- /app/metrics.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | import requests 3 | from user_agents import parse 4 | 5 | 6 | ### User Information 7 | def get_client_ip(): 8 | """Get the client IP address.""" 9 | if 'X-Forwarded-For' in request.headers: 10 | ip_address = request.headers['X-Forwarded-For'].split(',')[0] 11 | print(ip_address, flush=True) 12 | else: 13 | ip_address = request.remote_addr 14 | return ip_address 15 | 16 | 17 | 18 | 19 | def get_location(ip_address): 20 | """Get the approximate location of the user from their IP address.""" 21 | try: 22 | response = requests.get(f'http://ipinfo.io/{ip_address}/json') 23 | data = response.json() 24 | return f"{data['city']}, {data['region']}, {data['country']}" 25 | except Exception as e: 26 | print(f"Error fetching location for IP {ip_address}: {e}") 27 | return "Unknown" 28 | 29 | 30 | 31 | def detect_browser_and_os(user_agent): 32 | """Detect browser and operating system from User-Agent.""" 33 | ua = parse(user_agent) 34 | browser = f"{ua.browser.family} {ua.browser.version_string}" 35 | os = f"{ua.os.family} {ua.os.version_string}" 36 | return browser, os 37 | 38 | 39 | def detect_device(user_agent): 40 | """Detect the device based on the User-Agent string.""" 41 | if "Android" in user_agent: 42 | return "android" 43 | elif "iPhone" in user_agent or "iPad" in user_agent: 44 | return "ios" 45 | else: 46 | return "unknown" -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.8.2 2 | click==8.1.7 3 | Flask==3.0.3 4 | importlib_metadata==8.5.0 5 | itsdangerous==2.2.0 6 | Jinja2==3.1.4 7 | MarkupSafe==3.0.1 8 | pillow==10.4.0 9 | qrcode==8.0 10 | Werkzeug==3.0.4 11 | zipp==3.20.2 12 | requests==2.32.3 13 | user-agents==2.2.0 14 | validators==0.34.0 -------------------------------------------------------------------------------- /app/routes.py: -------------------------------------------------------------------------------- 1 | from flask import request, redirect, render_template, send_file, session, flash, url_for, jsonify 2 | import uuid 3 | import qrcode 4 | import io 5 | import base64 6 | from db import get_db 7 | from db import DATABASE_PATH 8 | from db import delete_qr_code_by_id 9 | from utils import is_valid_url, process_metrics 10 | from metrics import get_client_ip 11 | import os 12 | 13 | 14 | 15 | def setup_routes(app, SERVER_URL, PASSWORD): 16 | 17 | @app.route('/', methods=['GET', 'POST']) 18 | def index(): 19 | 20 | if request.method == 'POST': 21 | password = request.form.get('password') 22 | if password == PASSWORD: 23 | session['authenticated'] = True 24 | return redirect(url_for('index')) 25 | else: 26 | return "Invalid password", 403 27 | 28 | if 'authenticated' not in session: 29 | return render_template('index.html') 30 | 31 | """Display the form for entering the App Store and Play Store URLs.""" 32 | db = get_db() 33 | qr_codes = db.execute('SELECT * FROM qr_codes').fetchall() 34 | 35 | # Fetch tracking information for each QR code 36 | tracking_data = {} 37 | for code in qr_codes: 38 | tracking_data[code[0]] = db.execute('SELECT * FROM qr_code_tracking WHERE qr_code_id = ?', (code[0],)).fetchall() 39 | 40 | return render_template('index.html', qr_codes=[(code, tracking_data[code[0]]) for code in qr_codes]) 41 | 42 | @app.route('/create', methods=['POST']) 43 | def create(): 44 | """Generate a QR code with either custom URL content or app store links.""" 45 | title = request.form['title'] 46 | app_store_url = request.form.get('app_store_url') 47 | play_store_url = request.form.get('play_store_url') 48 | content = request.form.get('content') 49 | 50 | # Check if either content or app store URLs are provided 51 | if not content and not (app_store_url and play_store_url): 52 | flash('You must provide either a standard URL or both App Store and Play Store URLs.', 'danger') 53 | return render_template('index.html') 54 | 55 | # Check for invalid URL in the standard content 56 | if content and not is_valid_url(content): 57 | flash('Invalid URL provided for standard QR.', 'danger') 58 | return render_template('index.html') 59 | 60 | # Check for invalid URLs in the app store fields 61 | if not content: 62 | if not (is_valid_url(app_store_url) and is_valid_url(play_store_url)): 63 | flash('Invalid URLs for App Store or Play Store.', 'danger') 64 | return render_template('index.html') 65 | 66 | # Create a database connection and cursor 67 | db = get_db() 68 | cursor = db.cursor() 69 | 70 | # Generate a new UUID for the QR code 71 | qr_code_id = str(uuid.uuid4()) 72 | 73 | # Generate the appropriate QR code URL 74 | if content: # If content for a standard QR code is provided 75 | qr_url = f"{SERVER_URL}/redirect_standard?qr_code_id={qr_code_id}" # Redirect for standard QR 76 | cursor.execute('INSERT INTO qr_codes (id, title, content, app_store_url, play_store_url) VALUES (?, ?, ?, ?, ?)', 77 | (qr_code_id, title, content, "", "")) # App Store URLs are empty for standard QR codes 78 | else: # For app store links 79 | qr_url = f"{SERVER_URL}/redirect/{qr_code_id}" 80 | cursor.execute('INSERT INTO qr_codes (id, title, content, app_store_url, play_store_url) VALUES (?, ?, ?, ?, ?)', 81 | (qr_code_id, title, "", app_store_url, play_store_url)) 82 | 83 | db.commit() 84 | 85 | # Generate the QR code 86 | qr = qrcode.QRCode( 87 | version=1, 88 | error_correction=qrcode.constants.ERROR_CORRECT_L, 89 | box_size=10, 90 | border=4, 91 | ) 92 | qr.add_data(qr_url) 93 | qr.make(fit=True) 94 | img = qr.make_image(fill='black', back_color='white') 95 | 96 | # Save the QR code image 97 | img_buf = io.BytesIO() 98 | img.save(img_buf, 'PNG') 99 | img_buf.seek(0) 100 | 101 | # Update the database with the QR code image 102 | cursor.execute('UPDATE qr_codes SET qr_image = ? WHERE id = ?', (img_buf.getvalue(), qr_code_id)) 103 | db.commit() 104 | 105 | # Generate QR code URL to show in the template 106 | qr_code_url = f"{SERVER_URL}{url_for('show', code_id=qr_code_id)}" 107 | 108 | return render_template('index.html', qr_code_url=qr_code_url) 109 | 110 | @app.route('/redirect_standard') 111 | def redirect_standard(): 112 | """Redirect to the standard URL content.""" 113 | qr_code_id = request.args.get('qr_code_id') 114 | user_agent = request.headers.get('User-Agent') 115 | process_metrics(qr_code_id, user_agent) 116 | 117 | # Retrieve the URL content from the database 118 | db = get_db() 119 | qr_code_data = db.execute('SELECT content FROM qr_codes WHERE id = ?', (qr_code_id,)).fetchone() 120 | 121 | if qr_code_data is None or not is_valid_url(qr_code_data[0]): 122 | return "Invalid or missing URL", 400 123 | 124 | return redirect(qr_code_data[0]) 125 | 126 | 127 | @app.route('/show/') 128 | def show(code_id): 129 | """Serve the generated QR code image.""" 130 | db = get_db() 131 | qr_code_data = db.execute('SELECT qr_image FROM qr_codes WHERE id = ?', (code_id,)).fetchone() 132 | 133 | if qr_code_data is None: 134 | return "QR Code not found", 404 135 | 136 | buf = io.BytesIO(qr_code_data[0]) 137 | return send_file(buf, mimetype='image/png') 138 | 139 | @app.route('/redirect/') 140 | def redirect_to_store(qr_code_id): 141 | """Redirect the user to the appropriate store based on their device.""" 142 | db = get_db() 143 | 144 | # Fetch the app store and play store URLs from the database 145 | qr_code_data = db.execute('SELECT app_store_url, play_store_url FROM qr_codes WHERE id = ?', (qr_code_id,)).fetchone() 146 | 147 | if not qr_code_data: 148 | return "QR Code not found", 404 149 | 150 | app_store_url, play_store_url = qr_code_data 151 | 152 | # Get user agent and determine device type 153 | user_agent = request.headers.get('User-Agent') 154 | device = process_metrics(qr_code_id, user_agent) 155 | 156 | # Redirect based on device type 157 | if device == "android" and play_store_url: 158 | return redirect(play_store_url) 159 | elif device == "ios" and app_store_url: 160 | return redirect(app_store_url) 161 | else: 162 | return "Device not recognized or no URL provided", 400 163 | 164 | 165 | 166 | @app.template_filter('b64encode') 167 | def b64encode_filter(data): 168 | """Encode binary data to Base64 for embedding in HTML.""" 169 | if data: 170 | return base64.b64encode(data).decode('utf-8') 171 | return '' 172 | 173 | ## Admin Area 174 | from functools import wraps 175 | 176 | def require_authentication(f): 177 | @wraps(f) 178 | def decorated_function(*args, **kwargs): 179 | if 'authenticated' not in session: 180 | flash('You must be logged in to access this page.', 'danger') 181 | return redirect(url_for('admin')) 182 | return f(*args, **kwargs) 183 | return decorated_function 184 | 185 | @app.route('/admin', methods=['GET', 'POST']) 186 | def admin(): 187 | """Display QR code statistics (password protected).""" 188 | if request.method == 'POST': 189 | password = request.form.get('password') 190 | if password == PASSWORD: 191 | session['authenticated'] = True 192 | return redirect(url_for('admin')) 193 | else: 194 | return "Invalid password", 403 195 | 196 | if 'authenticated' not in session: 197 | return render_template('admin.html') 198 | 199 | db = get_db() 200 | qr_codes = db.execute('SELECT * FROM qr_codes').fetchall() 201 | 202 | tracking_data = {} 203 | for code in qr_codes: 204 | tracking_data[code[0]] = db.execute('SELECT * FROM qr_code_tracking WHERE qr_code_id = ?', (code[0],)).fetchall() 205 | 206 | qr_codes_with_url = [ 207 | (code, f"{SERVER_URL}/show/{code[0]}", tracking_data[code[0]]) 208 | for code in qr_codes 209 | ] 210 | 211 | 212 | return render_template('admin.html', qr_codes=qr_codes_with_url) 213 | 214 | @app.route('/delete/', methods=['GET']) 215 | @require_authentication 216 | def delete_qr_code(qr_code_id): 217 | """Delete a QR code entry from the database.""" 218 | delete_qr_code_by_id(qr_code_id) 219 | flash('QR code deleted successfully!', 'success') 220 | return redirect(url_for('admin')) 221 | 222 | @app.route('/download_db', methods=['GET']) 223 | @require_authentication 224 | def download_db(): 225 | if os.path.exists(DATABASE_PATH): 226 | return send_file(DATABASE_PATH, as_attachment=True) 227 | else: 228 | return "Database file not found.", 404 229 | 230 | @app.route('/logout') 231 | def logout(): 232 | """Logout and clear session.""" 233 | session.pop('authenticated', None) 234 | return redirect(url_for('admin')) 235 | -------------------------------------------------------------------------------- /app/static/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Arial', sans-serif; 3 | margin: 0; 4 | padding: 20px; 5 | background-color: #f0f0f0; 6 | color: #333; 7 | transition: background-color 0.3s, color 0.3s; 8 | justify-content: center; 9 | align-items: flex-start; 10 | min-height: 100vh; 11 | } 12 | 13 | .wrapper { 14 | max-width: 1200px; 15 | margin: auto; 16 | padding: 20px; 17 | background-color: white; 18 | border-radius: 8px; 19 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 20 | } 21 | 22 | h1, h2 { 23 | color: #333; 24 | } 25 | 26 | nav { 27 | display: flex; 28 | justify-content: space-between; 29 | align-items: center; 30 | background-color: #007bff; 31 | padding: 10px; 32 | } 33 | 34 | nav a { 35 | color: white; 36 | text-decoration: none; 37 | padding: 10px; 38 | } 39 | 40 | nav a:hover { 41 | background-color: #0056b3; 42 | border-radius: 4px; 43 | } 44 | 45 | .menu { 46 | display: flex; 47 | gap: 20px; 48 | } 49 | 50 | .menu-icon { 51 | display: none; 52 | font-size: 24px; 53 | cursor: pointer; 54 | } 55 | 56 | .flashes { 57 | margin: 20px 0; 58 | padding: 10px; 59 | background-color: #f8d7da; 60 | color: #721c24; 61 | border: 1px solid #f5c6cb; 62 | border-radius: 4px; 63 | list-style-type: none; 64 | } 65 | 66 | .card { 67 | background: white; 68 | border-radius: 8px; 69 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 70 | margin: 10px 0; 71 | padding: 20px; 72 | transition: transform 0.2s; 73 | } 74 | 75 | .card:hover { 76 | transform: scale(1.02); 77 | } 78 | 79 | .card-title { 80 | font-weight: bold; 81 | margin-bottom: 10px; 82 | } 83 | 84 | .card-content { 85 | margin-bottom: 15px; 86 | } 87 | 88 | .card img { 89 | max-width: 100px; 90 | height: auto; 91 | border-radius: 4px; 92 | } 93 | 94 | .url { 95 | word-wrap: break-word; 96 | margin: 10px 0; 97 | } 98 | 99 | .delete-button { 100 | background-color: #dc3545; 101 | color: white; 102 | border: none; 103 | cursor: pointer; 104 | padding: 10px 15px; 105 | border-radius: 4px; 106 | transition: background-color 0.3s; 107 | } 108 | 109 | .delete-button:hover { 110 | background-color: #c82333; 111 | } 112 | 113 | input[type="text"], input[type="submit"] { 114 | width: 100%; 115 | padding: 10px; 116 | margin: 10px 0; 117 | border-radius: 4px; 118 | border: 1px solid #ccc; 119 | box-sizing: border-box; 120 | } 121 | 122 | input[type="submit"] { 123 | background-color: #007bff; 124 | color: white; 125 | border: none; 126 | cursor: pointer; 127 | } 128 | 129 | input[type="submit"]:hover { 130 | background-color: #0056b3; 131 | } 132 | 133 | .qr-container { 134 | text-align: center; 135 | margin: 20px 0; 136 | } 137 | 138 | .qr-container img { 139 | max-width: 100%; 140 | width: 200px; 141 | height: auto; 142 | border-radius: 8px; 143 | } 144 | 145 | .qr-url { 146 | margin: 10px 0; 147 | font-size: 14px; 148 | color: #007bff; 149 | } 150 | 151 | .copy-button { 152 | background-color: #007bff; 153 | color: white; 154 | border: none; 155 | border-radius: 4px; 156 | padding: 5px 10px; 157 | cursor: pointer; 158 | } 159 | 160 | .copy-button:hover { 161 | background-color: #0056b3; 162 | } 163 | 164 | @media (max-width: 768px) { 165 | .menu { 166 | display: none; 167 | flex-direction: column; 168 | gap: 10px; 169 | background-color: #007bff; 170 | width: 100%; 171 | z-index: 1000; 172 | padding: 10px 0; 173 | } 174 | 175 | .menu.active { 176 | display: flex; 177 | } 178 | 179 | .menu-icon { 180 | display: block; 181 | } 182 | } 183 | 184 | /* Tab Styles */ 185 | .tab-container { 186 | margin-top: 20px; 187 | } 188 | 189 | .tab-buttons { 190 | display: flex; 191 | justify-content: space-around; 192 | } 193 | 194 | .tab-buttons button { 195 | flex: 1; 196 | padding: 10px; 197 | background-color: #007bff; 198 | color: white; 199 | border: none; 200 | cursor: pointer; 201 | transition: background-color 0.3s; 202 | } 203 | 204 | .tab-buttons button:hover { 205 | background-color: #0056b3; 206 | } 207 | 208 | .tab-buttons button.active { 209 | background-color: #0056b3; 210 | } 211 | 212 | .tab-content { 213 | display: none; 214 | margin-top: 20px; 215 | } 216 | 217 | .tab-content.active { 218 | display: block; 219 | } 220 | -------------------------------------------------------------------------------- /app/templates/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | AppLinkr - Code Statistics 9 | 10 | 11 | 12 | 13 |
14 | 25 | 26 | {% with messages = get_flashed_messages() %} 27 | {% if messages %} 28 |
    29 | {% for message in messages %} 30 |
  • {{ message }}
  • 31 | {% endfor %} 32 |
33 | {% endif %} 34 | {% endwith %} 35 | 36 |

QR Code Statistics

37 | 38 | {% if not session.get('authenticated') %} 39 |

Login

40 |
41 | 42 | 43 |
44 | {% else %} 45 | 46 |

Statistics

47 | {% for code, qr_code_url, records in qr_codes %} 48 |
49 |
{{ code[1] }}
50 |
51 | ID: {{ code[0] }}
52 | QR Code:
53 | 54 | QR Code 55 | 56 |
57 | Direct Redirect: {{ code[2] }}
58 | App Store URL: {{ code[3] }}
59 | Play Store URL: {{ code[4] }} 60 |
61 |
62 | Tracking Info: 63 |
    64 | Scans: {{ records|length }}
    65 | {% for record in records %} 66 |
  • {{ record[2] }} | Region: {{ record[5] }} | Browser: {{ record[6] }} | OS: {{ record[7] }} | Language: {{ record[8] }} | Referrer: {{ record[9] }}
  • 67 | {% endfor %} 68 |
69 |
70 |
71 |
72 | 73 |
74 |
75 | {% endfor %} 76 |
77 | 78 |
79 | {% endif %} 80 |
81 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AppLinkr - QR Codes for AppStores 8 | 9 | 10 | 11 |
12 | 13 | 24 | 25 | 26 | {% with messages = get_flashed_messages() %} 27 | {% if messages %} 28 |
    29 | {% for message in messages %} 30 |
  • {{ message }}
  • 31 | {% endfor %} 32 |
33 | {% endif %} 34 | {% endwith %} 35 | 36 | {% if not session.get('authenticated') %} 37 |

Login

38 |
39 | 40 | 41 |
42 | {% else %} 43 | 44 | 45 |
46 |
47 | 48 | 49 |
50 | 51 | 52 |
53 |
54 |
55 |

56 | 57 |
58 |

59 | 60 |
61 |

62 | 63 | 64 |
65 |
66 | 67 | 68 |
69 |
70 |
71 |

72 | 73 |
74 |

75 | 76 | 77 |
78 |
79 | 80 | {% if qr_code_url %} 81 |
82 |

Your QR Code:

83 | QR Code 84 |
85 | {{ qr_code_url }} 86 | 87 |
88 |
89 |
90 | {% endif %} 91 | {% endif %} 92 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /app/templates/static/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Arial', sans-serif; 3 | margin: 0; 4 | padding: 20px; 5 | background-color: #f0f0f0; 6 | color: #333; 7 | transition: background-color 0.3s, color 0.3s; 8 | justify-content: center; 9 | align-items: flex-start; 10 | min-height: 100vh; 11 | } 12 | 13 | .wrapper { 14 | max-width: 1200px; 15 | margin: auto; 16 | padding: 20px; 17 | background-color: white; 18 | border-radius: 8px; 19 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 20 | } 21 | 22 | h1, h2 { 23 | color: #333; 24 | } 25 | 26 | nav { 27 | display: flex; 28 | justify-content: space-between; 29 | align-items: center; 30 | background-color: #007bff; 31 | padding: 10px; 32 | } 33 | 34 | nav a { 35 | color: white; 36 | text-decoration: none; 37 | padding: 10px; 38 | } 39 | 40 | nav a:hover { 41 | background-color: #0056b3; 42 | border-radius: 4px; 43 | } 44 | 45 | .menu { 46 | display: flex; 47 | gap: 20px; 48 | } 49 | 50 | .menu-icon { 51 | display: none; 52 | font-size: 24px; 53 | cursor: pointer; 54 | } 55 | 56 | .flashes { 57 | margin: 20px 0; 58 | padding: 10px; 59 | background-color: #f8d7da; 60 | color: #721c24; 61 | border: 1px solid #f5c6cb; 62 | border-radius: 4px; 63 | list-style-type: none; 64 | } 65 | 66 | .card { 67 | background: white; 68 | border-radius: 8px; 69 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 70 | margin: 10px 0; 71 | padding: 20px; 72 | transition: transform 0.2s; 73 | } 74 | 75 | .card:hover { 76 | transform: scale(1.02); 77 | } 78 | 79 | .card-title { 80 | font-weight: bold; 81 | margin-bottom: 10px; 82 | } 83 | 84 | .card-content { 85 | margin-bottom: 15px; 86 | } 87 | 88 | .card img { 89 | max-width: 100px; 90 | height: auto; 91 | border-radius: 4px; 92 | } 93 | 94 | .url { 95 | word-wrap: break-word; 96 | margin: 10px 0; 97 | } 98 | 99 | .delete-button { 100 | background-color: #dc3545; 101 | color: white; 102 | border: none; 103 | cursor: pointer; 104 | padding: 10px 15px; 105 | border-radius: 4px; 106 | transition: background-color 0.3s; 107 | } 108 | 109 | .delete-button:hover { 110 | background-color: #c82333; 111 | } 112 | 113 | input[type="text"], input[type="submit"] { 114 | width: 100%; 115 | padding: 10px; 116 | margin: 10px 0; 117 | border-radius: 4px; 118 | border: 1px solid #ccc; 119 | box-sizing: border-box; 120 | } 121 | 122 | input[type="submit"] { 123 | background-color: #007bff; 124 | color: white; 125 | border: none; 126 | cursor: pointer; 127 | } 128 | 129 | input[type="submit"]:hover { 130 | background-color: #0056b3; 131 | } 132 | 133 | .qr-container { 134 | text-align: center; 135 | margin: 20px 0; 136 | } 137 | 138 | .qr-container img { 139 | max-width: 100%; 140 | width: 200px; 141 | height: auto; 142 | border-radius: 8px; 143 | } 144 | 145 | .qr-url { 146 | margin: 10px 0; 147 | font-size: 14px; 148 | color: #007bff; 149 | } 150 | 151 | .copy-button { 152 | background-color: #007bff; 153 | color: white; 154 | border: none; 155 | border-radius: 4px; 156 | padding: 5px 10px; 157 | cursor: pointer; 158 | } 159 | 160 | .copy-button:hover { 161 | background-color: #0056b3; 162 | } 163 | 164 | @media (max-width: 768px) { 165 | .menu { 166 | display: none; 167 | flex-direction: column; 168 | gap: 10px; 169 | background-color: #007bff; 170 | width: 100%; 171 | z-index: 1000; 172 | padding: 10px 0; 173 | } 174 | 175 | .menu.active { 176 | display: flex; 177 | } 178 | 179 | .menu-icon { 180 | display: block; 181 | } 182 | } 183 | 184 | /* Tab Styles */ 185 | .tab-container { 186 | margin-top: 20px; 187 | } 188 | 189 | .tab-buttons { 190 | display: flex; 191 | justify-content: space-around; 192 | } 193 | 194 | .tab-buttons button { 195 | flex: 1; 196 | padding: 10px; 197 | background-color: #007bff; 198 | color: white; 199 | border: none; 200 | cursor: pointer; 201 | transition: background-color 0.3s; 202 | } 203 | 204 | .tab-buttons button:hover { 205 | background-color: #0056b3; 206 | } 207 | 208 | .tab-buttons button.active { 209 | background-color: #0056b3; 210 | } 211 | 212 | .tab-content { 213 | display: none; 214 | margin-top: 20px; 215 | } 216 | 217 | .tab-content.active { 218 | display: block; 219 | } 220 | -------------------------------------------------------------------------------- /app/utils.py: -------------------------------------------------------------------------------- 1 | import validators 2 | from db import get_db 3 | from metrics import detect_device, get_client_ip, get_location, detect_browser_and_os 4 | from flask import request 5 | 6 | 7 | def is_valid_url(url): 8 | """Validate if the given content is a valid URL.""" 9 | return validators.url(url) 10 | 11 | def process_metrics(qr_code_id, user_agent): 12 | """Collect tracking information and save it to the database.""" 13 | device = detect_device(user_agent) 14 | language = request.headers.get('Accept-Language', 'Unknown') 15 | ip_address = get_client_ip() 16 | referrer = request.headers.get('Referer', 'Direct Access') 17 | region = get_location(ip_address) 18 | browser, os = detect_browser_and_os(user_agent) 19 | 20 | db = get_db() 21 | db.execute('''INSERT INTO qr_code_tracking (qr_code_id, device_type, ip_address, region, browser, os, language, referrer) 22 | VALUES (?, ?, ?, ?, ?, ?, ?, ?)''', 23 | (qr_code_id, device, ip_address, region, browser, os, language, referrer)) 24 | db.commit() 25 | return device 26 | -------------------------------------------------------------------------------- /images/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schech1/AppLinkr/10b46bf6bf77b64c968dc2a72d2bd70db1da67b1/images/admin.png -------------------------------------------------------------------------------- /images/generate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schech1/AppLinkr/10b46bf6bf77b64c968dc2a72d2bd70db1da67b1/images/generate.png --------------------------------------------------------------------------------