Ver 1
This commit is contained in:
parent
358b007ff9
commit
8ace6f551b
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
*.db
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
22
README.md
22
README.md
@ -1,3 +1,23 @@
|
||||
# shortn
|
||||
HTML/CSS/JS Framework Free ✅ • Lightweight ✅ • Functional ✅ • No plaintext passwords ✅
|
||||
|
||||
Simple link shortener with flask
|
||||
|
||||
Simple link shortener built with flask.
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
git clone https://git.confest.im/boyan_k/shortn
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
## TODOs
|
||||
- [x] basic UI (to add links)
|
||||
- [x] basic auth for UI
|
||||
- [x] sqlite3 to store links
|
||||
- [ ] responsive?
|
||||
- [ ] dockerize
|
||||
|
144
src/app.py
Normal file
144
src/app.py
Normal file
@ -0,0 +1,144 @@
|
||||
from flask import Flask, request, redirect, url_for, render_template, flash, session, jsonify
|
||||
import sqlite3
|
||||
import os
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import re
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'your_secret_key' # Replace with a strong secret key
|
||||
|
||||
# Database initialization
|
||||
DATABASE = 'database.db'
|
||||
if not os.path.exists(DATABASE):
|
||||
raise FileNotFoundError(f"Database file not found. Run `python manage_users.py init` to create it.")
|
||||
|
||||
def init_db():
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS redirects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dest_link TEXT NOT NULL,
|
||||
custom_link TEXT UNIQUE
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
|
||||
# Helper function to interact with the database
|
||||
def query_db(query, args=(), one=False):
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, args)
|
||||
rv = cursor.fetchall()
|
||||
conn.commit()
|
||||
return (rv[0] if rv else None) if one else rv
|
||||
|
||||
@app.route('/redirects', methods=['GET', 'POST'])
|
||||
def redirects_list():
|
||||
if 'username' not in session: # Ensure the user is logged in
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if request.method == 'POST': # Handle deletion
|
||||
redirect_id = request.form.get('redirect_id')
|
||||
if redirect_id:
|
||||
query_db('DELETE FROM redirects WHERE id = ?', (redirect_id,))
|
||||
flash('Redirect deleted successfully!')
|
||||
return redirect(url_for('redirects_list'))
|
||||
|
||||
# Fetch all active redirects
|
||||
redirects = query_db('SELECT id, dest_link, custom_link FROM redirects')
|
||||
return render_template('redirects.html', redirects=redirects)
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
if 'username' in session:
|
||||
return redirect(url_for('form'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
user = query_db('SELECT * FROM users WHERE username = ?', (username,), one=True)
|
||||
if user and check_password_hash(user[2], password):
|
||||
session['username'] = username
|
||||
return redirect(url_for('form'))
|
||||
else:
|
||||
flash('Invalid credentials')
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.pop('username', None)
|
||||
return redirect(url_for('login'))
|
||||
|
||||
def verify_link(link:str) -> bool:
|
||||
if not link.startswith('http://') and not link.startswith('https://'):
|
||||
link = 'http://' + link
|
||||
reg = r'^(https?:\/\/)(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(:\d+)?(\/[a-zA-Z0-9@:%_\+.~#?&//=]*)?$'
|
||||
return False if not re.match(reg, link) else True
|
||||
|
||||
@app.route('/form', methods=['GET', 'POST'])
|
||||
def form():
|
||||
if 'username' not in session:
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
dest_link = request.form['dest_link']
|
||||
if not verify_link(dest_link):
|
||||
flash('Invalid URL. Make sure it starts with http:// or https://')
|
||||
return render_template('nohack.html')
|
||||
|
||||
custom_link = request.form.get('custom_link', None)
|
||||
|
||||
try:
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT INTO redirects (dest_link, custom_link) VALUES (?, ?)',
|
||||
(dest_link, custom_link))
|
||||
conn.commit()
|
||||
flash('Redirect created successfully!')
|
||||
return render_template('success.html', link=custom_link if custom_link else f"/r/{cursor.lastrowid}")
|
||||
except sqlite3.IntegrityError:
|
||||
flash('Custom link already exists. Try another.')
|
||||
|
||||
# return render_template('form.html',
|
||||
# return template with user logged in flag
|
||||
return render_template('form.html', user_logged_in=True)
|
||||
|
||||
@app.route('/r/<custom_link>')
|
||||
def redirect_to_custom(custom_link):
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
if result := query_db('SELECT dest_link FROM redirects WHERE custom_link = ?', (custom_link,), one=True):
|
||||
# Redirect to external link
|
||||
return redirect(result[0])
|
||||
else:
|
||||
return 'Redirect not found', 404
|
||||
|
||||
# For testing purpose, use a method to create users
|
||||
@app.route('/create_user', methods=['POST'])
|
||||
def create_user():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = generate_password_hash(request.form['password'])
|
||||
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute('INSERT INTO users (username, password) VALUES (?, ?)', (username, password))
|
||||
conn.commit()
|
||||
return 'User created successfully!'
|
||||
except sqlite3.IntegrityError:
|
||||
return 'User already exists.'
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
app.run(debug=True)
|
98
src/manage_users.py
Normal file
98
src/manage_users.py
Normal file
@ -0,0 +1,98 @@
|
||||
import sqlite3
|
||||
import argparse
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import getpass
|
||||
import sys
|
||||
|
||||
DATABASE = 'database.db'
|
||||
|
||||
# Function to initialize the database (if not already done)
|
||||
def init_db():
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS redirects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dest_link TEXT NOT NULL,
|
||||
custom_link TEXT UNIQUE
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
|
||||
# Function to add a new user
|
||||
def add_user(username, password):
|
||||
hashed_password = generate_password_hash(password)
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute('INSERT INTO users (username, password) VALUES (?, ?)', (username, hashed_password))
|
||||
conn.commit()
|
||||
print(f"User '{username}' created successfully.")
|
||||
except sqlite3.IntegrityError:
|
||||
print(f"Error: User '{username}' already exists.")
|
||||
|
||||
# Function to delete a user
|
||||
def delete_user(username):
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DELETE FROM users WHERE username = ?', (username,))
|
||||
conn.commit()
|
||||
if cursor.rowcount > 0:
|
||||
print(f"User '{username}' deleted successfully.")
|
||||
else:
|
||||
print(f"Error: User '{username}' not found.")
|
||||
|
||||
# Function to list all users
|
||||
def list_users():
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, username FROM users')
|
||||
users = cursor.fetchall()
|
||||
if users:
|
||||
print("Users:")
|
||||
for user in users:
|
||||
print(f" - ID: {user[0]}, Username: {user[1]}")
|
||||
else:
|
||||
print("No users found.")
|
||||
|
||||
# CLI argument parser
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="User management CLI for the Flask app.")
|
||||
parser.add_argument('command', choices=['add', 'delete', 'list', 'init'], help="Command to execute.")
|
||||
parser.add_argument('--username', help="Username of the user.")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Execute the appropriate function based on the command
|
||||
if args.command == 'init':
|
||||
init_db()
|
||||
print("Database initialized.")
|
||||
elif args.command == 'add':
|
||||
user = input("Enter username: ")
|
||||
password = getpass.getpass("Enter password: ")
|
||||
verify = getpass.getpass("Re-enter password: ")
|
||||
if password != verify:
|
||||
print("Error: Passwords do not match.")
|
||||
sys.exit(1)
|
||||
add_user(user, password)
|
||||
elif args.command == 'delete':
|
||||
if not args.username:
|
||||
print("Error: --username is required for 'delete' command.")
|
||||
else:
|
||||
delete_user(args.username)
|
||||
elif args.command == 'list':
|
||||
try:
|
||||
list_users()
|
||||
except sqlite3.OperationalError:
|
||||
print("Error: Database not initialized. Run 'init' command first.")
|
||||
else:
|
||||
print("Invalid command. Use --help for usage information.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
46
src/static/css/listing.css
Normal file
46
src/static/css/listing.css
Normal file
@ -0,0 +1,46 @@
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: #f4f4f4;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
table tr:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background-color: #ff4d4d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.delete-button:active {
|
||||
background-color: #d93636;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
background-color: #d93636;
|
||||
}
|
83
src/static/css/style.css
Normal file
83
src/static/css/style.css
Normal file
@ -0,0 +1,83 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap');
|
||||
body {
|
||||
/* Soft gray background color */
|
||||
background-color: #f5f5f5;
|
||||
font-family: "Fira Code", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
nav {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
top:0;
|
||||
position: sticky;
|
||||
padding: 10px 0;
|
||||
z-index: 1000;
|
||||
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
background-color: #444; /* Slightly lighter background on hover */
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px; /* Center the content */
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
margin-top: 40px;
|
||||
/* Put on very bottom without making absolute */
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #ffc107;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
.bad-wrong {
|
||||
color: #ff4d4d;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #28a745;
|
||||
}
|
65
src/static/css/textForm.css
Normal file
65
src/static/css/textForm.css
Normal file
@ -0,0 +1,65 @@
|
||||
form {
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
max-width: 400px;
|
||||
margin: 20px auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
form h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
form input[type="text"], form input[type="url"], form input[type="password"], form button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
form input[type="text"]:focus, form input[type="url"]:focus {
|
||||
border-color: #007bff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 4px rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
|
||||
form button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
form button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
form .error-message {
|
||||
color: #ff4d4d;
|
||||
font-size: 14px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
form .success-message {
|
||||
color: #28a745;
|
||||
font-size: 14px;
|
||||
margin-top: -10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
form .optional {
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
text-align: right;
|
||||
}
|
BIN
src/static/img/cry.png
Normal file
BIN
src/static/img/cry.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
src/static/img/favicon.png
Normal file
BIN
src/static/img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
77
src/static/js/validLink.js
Normal file
77
src/static/js/validLink.js
Normal file
@ -0,0 +1,77 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const form = document.querySelector('form');
|
||||
const destLinkInput = form.querySelector('input[name="dest_link"]');
|
||||
const customLinkInput = form.querySelector('input[name="custom_link"]');
|
||||
const destLinkError = document.createElement('p');
|
||||
const customLinkError = document.createElement('p');
|
||||
|
||||
destLinkError.classList.add('error-message');
|
||||
customLinkError.classList.add('error-message');
|
||||
|
||||
// Append error messages right after inputs
|
||||
destLinkInput.parentNode.insertBefore(destLinkError, destLinkInput.nextSibling);
|
||||
customLinkInput.parentNode.insertBefore(customLinkError, customLinkInput.nextSibling);
|
||||
|
||||
// Function to validate URLs
|
||||
const isValidURL = (url) => {
|
||||
const pattern = new RegExp(
|
||||
'^(https?:\\/\\/)?' + // Protocol (http or https)
|
||||
'(([a-zA-Z0-9_-]+\\.)+[a-zA-Z]{2,6})' + // Domain name
|
||||
'(\\/[a-zA-Z0-9@:%_\\+.~#?&//=]*)?$', // Path, query, or fragment
|
||||
'i'
|
||||
);
|
||||
return pattern.test(url);
|
||||
};
|
||||
|
||||
// Function to add https:// if missing
|
||||
const addHttpsIfMissing = (url) => {
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
return 'https://' + url;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
// Validate destination link on input
|
||||
destLinkInput.addEventListener('blur', () => {
|
||||
// Auto-correct the URL if missing protocol
|
||||
destLinkInput.value = addHttpsIfMissing(destLinkInput.value);
|
||||
|
||||
// Validate URL
|
||||
if (!isValidURL(destLinkInput.value)) {
|
||||
destLinkError.textContent = 'Please enter a valid URL (e.g., https://example.com)';
|
||||
} else {
|
||||
destLinkError.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Validate custom link for valid characters
|
||||
customLinkInput.addEventListener('input', () => {
|
||||
if (customLinkInput.value && /[^a-zA-Z0-9_-]/.test(customLinkInput.value)) {
|
||||
customLinkError.textContent = 'Custom link can only contain letters, numbers, dashes, and underscores.';
|
||||
} else {
|
||||
customLinkError.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Validate form on submit
|
||||
form.addEventListener('submit', (event) => {
|
||||
let isValid = true;
|
||||
|
||||
// Auto-correct the URL if missing protocol
|
||||
destLinkInput.value = addHttpsIfMissing(destLinkInput.value);
|
||||
|
||||
if (!isValidURL(destLinkInput.value)) {
|
||||
destLinkError.textContent = 'Please enter a valid URL (e.g., https://example.com)';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (customLinkInput.value && /[^a-zA-Z0-9_-]/.test(customLinkInput.value)) {
|
||||
customLinkError.textContent = 'Custom link can only contain letters, numbers, dashes, and underscores.';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
event.preventDefault(); // Prevent form submission if validation fails
|
||||
}
|
||||
});
|
||||
});
|
51
src/templates/base.html
Normal file
51
src/templates/base.html
Normal file
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Shortn is a simple URL shortener service."
|
||||
/>
|
||||
|
||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}" />
|
||||
<title>{% block title %}Shortn{% endblock %}</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ url_for('static', filename='css/style.css') }}"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://use.fontawesome.com/releases/v5.8.1/css/all.css"
|
||||
/>
|
||||
<nav>
|
||||
<a href="{{ url_for('form') }}">
|
||||
<i class="fas fa-plus"></i> Create Redirect</a
|
||||
>
|
||||
|
||||
<a href="{{ url_for('redirects_list') }}">
|
||||
<i class="fas fa-list"></i> Manage Redirects</a
|
||||
>
|
||||
{% if user_logged_in %}
|
||||
<a href="{{ url_for('logout') }}">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout</a
|
||||
>
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}">
|
||||
<i class="fas fa-sign-in-alt"></i> Login</a
|
||||
>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content">{% block content %}{% endblock %}</div>
|
||||
</div>
|
||||
<footer>
|
||||
<p>Shortn 2024, <a href="confest.im">confestim</a></p>
|
||||
</footer>
|
||||
{% block footer %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
28
src/templates/form.html
Normal file
28
src/templates/form.html
Normal file
@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/textForm.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Create Redirect{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Create a Redirect</h1>
|
||||
<form method="post">
|
||||
<input type="text" name="dest_link" placeholder="https://example.com" required><br>
|
||||
<input type="text" name="custom_link" placeholder="https://example2.com"><br>
|
||||
<button type="submit">Create Redirect</button>
|
||||
</form>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<p class="bad-wrong">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
{{ message }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<script src="{{ url_for('static', filename='js/validLink.js') }}" defer></script>
|
||||
|
||||
{% endblock %}
|
21
src/templates/login.html
Normal file
21
src/templates/login.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %} {% block head %}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ url_for('static', filename='css/textForm.css') }}"
|
||||
/>
|
||||
{% endblock %} {% block title %}Login{% endblock %} {% block content %}
|
||||
<h1>Login</h1>
|
||||
<form method="post">
|
||||
<input type="text" name="username" placeholder="Username" required /><br />
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
/><br />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{% with messages = get_flashed_messages() %} {% if messages %} {% for message in
|
||||
messages %}
|
||||
<p class="bad-wrong">{{ message }}</p>
|
||||
{% endfor %} {% endif %} {% endwith %} {% endblock %}
|
15
src/templates/nohack.html
Normal file
15
src/templates/nohack.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
|
||||
{% block title %}Please Don't Hack Me{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Please Don't Hack Me</h1>
|
||||
<img src="{{ url_for('static', filename='img/cry.png') }}" alt="Crying emoji">
|
||||
<p>Your request <span class="bad-wrong"> did not go through </span> Redirecting...</p>
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
window.location.href = "{{ url_for('form') }}";
|
||||
}, 5000);
|
||||
</script>
|
||||
{% endblock %}
|
54
src/templates/redirects.html
Normal file
54
src/templates/redirects.html
Normal file
@ -0,0 +1,54 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/listing.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Manage Redirects{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Manage Redirects</h1>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<p class="success"><i class="fas fa-check"></i>{{ message }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% if redirects %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Destination Link</th>
|
||||
<th>Custom Link</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for redirect in redirects %}
|
||||
<tr>
|
||||
<td>{{ redirect[0] }}</td>
|
||||
<td>{{ redirect[1] }}</td>
|
||||
<td>
|
||||
{% if redirect[2] %}
|
||||
<a href="/r/{{ redirect[2] }}">{{ redirect[2] }}</a>
|
||||
{% else %}
|
||||
<a href="/r/{{ redirect[0] }}/">{{ redirect[0] }}</a>
|
||||
{% endif %}
|
||||
<td>
|
||||
<form method="post" style="display: flex;">
|
||||
<input type="hidden" name="redirect_id" value="{{ redirect[0] }}">
|
||||
<button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No redirects found.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
11
src/templates/success.html
Normal file
11
src/templates/success.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Success{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="success">Link Created Successfully!</h1>
|
||||
<p> <a href="/r/{{ link }}">{{ link }}</a></p>
|
||||
<p> <a href="{{ url_for('form') }}">Create Another Redirect?</a></p>
|
||||
|
||||
{% endblock %}
|
28
src/test.py
Normal file
28
src/test.py
Normal file
@ -0,0 +1,28 @@
|
||||
import sqlite3
|
||||
|
||||
DATABASE = 'database.db'
|
||||
|
||||
# Function to insert links into the redirects table
|
||||
def add_links():
|
||||
links = [
|
||||
{
|
||||
"dest_link": f"https://example{num}.com",
|
||||
"custom_link": f"custom{num}"
|
||||
} for num in range(1, 31)
|
||||
]
|
||||
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
for link in links:
|
||||
try:
|
||||
cursor.execute(
|
||||
'INSERT INTO redirects (dest_link, custom_link) VALUES (?, ?)',
|
||||
(link["dest_link"], link["custom_link"])
|
||||
)
|
||||
except sqlite3.IntegrityError:
|
||||
print(f"Custom link {link['custom_link']} already exists.")
|
||||
conn.commit()
|
||||
print(f"Added {len(links)} links to the database.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
add_links()
|
Loading…
x
Reference in New Issue
Block a user