├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── requirements.txt ├── run.py ├── runtime.txt └── tiny0 ├── __init__.py ├── config.py ├── forms.py ├── models.py ├── routes.py ├── static ├── favicon.ico ├── font.css └── style.css ├── templates ├── clicks.html ├── donate.html ├── error.html ├── index.html ├── layout.html ├── lookup.html ├── original-url.html ├── removed.html ├── report.html ├── thanks.html ├── tracker.html └── url.html └── token.py /.gitignore: -------------------------------------------------------------------------------- 1 | tiny0/__pycache__ 2 | tiny0/database.db 3 | venv 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Muhammed Ali Dilek 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn run:app -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | Each URL that is submitted goes through a simple check for validity and/or added http:// before it to redirect successful. 4 | After that, a base 64 string is generated and added to a SQLite database with the corresponding URL that was submitted. 5 | The user is then given a short URL that is formatted as: WEBSITE_DOMAIN/token, the token being the base 64 string. 6 | Whenever this URL is visited the user will get redirected to the tokens corresponding URL in the database. 7 | 8 | # Run locally 9 | 10 | **Highly encouraged to create a python environment first.** 11 | 12 | Clone the repository: 13 | 14 | $ git clone https://github.com/xemeds/tiny0.git 15 | 16 | Move into the cloned folder and install the required libraries: 17 | 18 | $ cd tiny0 19 | $ pip install -r requirements.txt 20 | 21 | After that run with: 22 | 23 | $ python run.py 24 | 25 | Visit the below URL to view the flask app: 26 | 27 | 127.0.0.1:5000 28 | 29 | **NOTE:** When running locally all redirects will also be local. 30 | 31 | # Deploying 32 | 33 | If you do not have a dedicated server, I highly recommend using [Linode](https://www.linode.com/), [Heroku](https://www.heroku.com/) or [PythonAnywhere](https://www.pythonanywhere.com/) to host your application. 34 | 35 | Before deploying, make sure to set the following environment variables: 36 | 37 | $ export WEBSITE_DOMAIN= 38 | $ export SECRET_KEY= 39 | $ export DEBUG= 40 | $ export SQLALCHEMY_DATABASE_URI= 41 | 42 | If not they will default to the following values: 43 | 44 | WEBSITE_DOMAIN=127.0.0.1:5000 45 | SECRET_KEY=SECRET_KEY 46 | DEBUG=true 47 | SQLALCHEMY_DATABASE_URI=sqlite:///database.db 48 | 49 | # License 50 | 51 | This project is under the [MIT](https://github.com/xemeds/tiny0/blob/master/LICENSE) license. 52 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask==1.1.2 2 | SQLAlchemy==1.3.23 3 | flask-sqlalchemy==2.4.4 4 | flask-wtf==0.14.3 5 | gunicorn==20.1.0 6 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from tiny0 import app 2 | from tiny0.config import DEBUG 3 | 4 | if __name__ == '__main__': 5 | app.run(debug=DEBUG) 6 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.5 -------------------------------------------------------------------------------- /tiny0/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | from tiny0.config import SECRET_KEY, SQLALCHEMY_DATABASE_URI 4 | 5 | app = Flask(__name__) 6 | 7 | app.config['SECRET_KEY'] = SECRET_KEY 8 | app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI 9 | 10 | db = SQLAlchemy(app) 11 | 12 | # Initialize the database 13 | from tiny0.models import URL 14 | db.create_all() 15 | 16 | from tiny0 import routes 17 | -------------------------------------------------------------------------------- /tiny0/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from distutils.util import strtobool 3 | 4 | WEBSITE_DOMAIN = os.environ.get("WEBSITE_DOMAIN") 5 | if not WEBSITE_DOMAIN: 6 | WEBSITE_DOMAIN = "127.0.0.1:5000" 7 | 8 | SECRET_KEY = os.environ.get("SECRET_KEY") 9 | if not SECRET_KEY: 10 | SECRET_KEY = "SECRET_KEY" 11 | 12 | SQLALCHEMY_DATABASE_URI = os.environ.get("SQLALCHEMY_DATABASE_URI") 13 | if not SQLALCHEMY_DATABASE_URI: 14 | SQLALCHEMY_DATABASE_URI = "sqlite:///database.db" 15 | 16 | DEBUG = os.environ.get("DEBUG") 17 | if not DEBUG: 18 | DEBUG = "true" 19 | 20 | DEBUG = strtobool(DEBUG) 21 | -------------------------------------------------------------------------------- /tiny0/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, SubmitField, TextAreaField, ValidationError 3 | from wtforms.validators import DataRequired, Length, Optional 4 | from tiny0.config import WEBSITE_DOMAIN 5 | from tiny0 import db 6 | from tiny0.models import URL 7 | 8 | # Validates a url 9 | def validate_URL(form, field): 10 | # Make sure the url is not too short or long 11 | if len(field.data) < 4 or len(field.data) > 2000: 12 | return 13 | 14 | # If the url contains spaces or does not have any dots 15 | if (" " in field.data) or not("." in field.data): 16 | # Raise a ValidationError 17 | raise ValidationError("Invalid URL") 18 | 19 | # If the url starts with a dot after http:// or after https:// or just starts with a dot 20 | if field.data.lower().startswith("http://.") or field.data.lower().startswith("https://.") or field.data.startswith("."): 21 | # Raise a ValidationError 22 | raise ValidationError("Invalid URL") 23 | 24 | # If the url starts with a slash after http:// or after https:// or just starts with a slash 25 | if field.data.lower().startswith("http:///") or field.data.lower().startswith("https:///") or field.data.startswith("/"): 26 | # Raise a ValidationError 27 | raise ValidationError("Invalid URL") 28 | 29 | # If the url ends with a dot and it is the only dot 30 | if field.data.endswith(".") and field.data.count(".") == 1: 31 | # Raise a ValidationError 32 | raise ValidationError("Invalid URL") 33 | 34 | # If the url contains the websites domain 35 | if WEBSITE_DOMAIN in field.data.lower(): 36 | # Raise a ValidationError 37 | raise ValidationError("Invalid URL") 38 | 39 | # If the URL does not start with http:// and https:// 40 | if not(field.data.lower().startswith("http://")) and not(field.data.lower().startswith("https://")): 41 | # Add http:// to the beginning of the URL 42 | field.data = "http://" + field.data 43 | 44 | # Validates a token 45 | def validate_token(form, field): 46 | # Make sure the token is not too short or long 47 | if len(field.data) < 6 or len(field.data) > 16: 48 | return 49 | 50 | # If the token is the same as a pages route 51 | if field.data == "tracker" or field.data == "lookup" or field.data == "report" or field.data == "donate" or field.data == "removed": 52 | # Raise a ValidationError 53 | raise ValidationError("Token already exists") 54 | 55 | # For each character in the token 56 | for char in field.data: 57 | # If it is not a valid character 58 | if not(char.isalpha()) and not(char.isdigit()) and not(char == "_") and not(char == '-'): 59 | # Raise a ValidationError 60 | raise ValidationError("Token contains invalid characters") 61 | 62 | # If the token exists in the database 63 | if db.session.query(db.session.query(URL).filter_by(token=field.data).exists()).scalar(): 64 | # Raise a ValidationError 65 | raise ValidationError("Token already exists") 66 | 67 | # Validates a short url 68 | def validate_short_URL(form, field): 69 | # Make sure the short url is not too short or long 70 | if len(field.data) < (len(WEBSITE_DOMAIN) + 7) or len(field.data) > (len(WEBSITE_DOMAIN) + 25): 71 | return 72 | 73 | # If the start of the short url is not valid 74 | if (not(field.data.lower().startswith(WEBSITE_DOMAIN + "/")) 75 | and not(field.data.lower().startswith("http://" + WEBSITE_DOMAIN + "/")) 76 | and not(field.data.lower().startswith("https://" + WEBSITE_DOMAIN + "/"))): 77 | # Raise a ValidationError 78 | raise ValidationError("Invalid short URL") 79 | 80 | # Get the token of the short url 81 | if field.data.lower().startswith(WEBSITE_DOMAIN + "/"): 82 | token = field.data[len(WEBSITE_DOMAIN) + 1:] 83 | 84 | elif field.data.lower().startswith("http://" + WEBSITE_DOMAIN + "/"): 85 | token = field.data[len(WEBSITE_DOMAIN) + 8:] 86 | 87 | elif field.data.lower().startswith("https://" + WEBSITE_DOMAIN + "/"): 88 | token = field.data[len(WEBSITE_DOMAIN) + 9:] 89 | 90 | # If the token of the short url does not exist in the database 91 | if not db.session.query(db.session.query(URL).filter_by(token=token).exists()).scalar(): 92 | # Raise a ValidationError 93 | raise ValidationError("That short URL does not exists") 94 | 95 | # After all the validation is done set the forms url value as the token 96 | field.data = token 97 | 98 | class URLForm(FlaskForm): 99 | url = StringField(validators=[DataRequired(), Length(min=4, max=2000, message="Invalid URL length"), validate_URL]) 100 | 101 | token = StringField(validators=[Optional(), Length(min=6, max=16, message="Invalid token length"), validate_token]) 102 | 103 | submit = SubmitField("Shorten") 104 | 105 | class ShortURLForm(FlaskForm): 106 | url = StringField(validators=[DataRequired(), Length(min=len(WEBSITE_DOMAIN) + 7, max=len(WEBSITE_DOMAIN) + 25, message="Invalid short URL"), validate_short_URL]) 107 | 108 | submit = SubmitField("Submit") 109 | 110 | class ReportForm(FlaskForm): 111 | url = StringField(validators=[DataRequired(), Length(min=len(WEBSITE_DOMAIN) + 7, max=len(WEBSITE_DOMAIN) + 25, message="Invalid short URL"), validate_short_URL]) 112 | 113 | message = TextAreaField(validators=[DataRequired(), Length(1, 200, message="Message too short or too long")]) 114 | 115 | submit = SubmitField("Submit") 116 | -------------------------------------------------------------------------------- /tiny0/models.py: -------------------------------------------------------------------------------- 1 | from tiny0 import db 2 | 3 | class URL(db.Model): 4 | id = db.Column(db.Integer, primary_key=True) 5 | token = db.Column(db.String(16), index=True, unique=True, nullable=False) 6 | url = db.Column(db.String(2000), nullable=False) 7 | clicks = db.Column(db.Integer, nullable=False, default=0) 8 | 9 | def __repr__(self): 10 | return f"'{self.id}' '{self.token}' '{self.url}' '{self.clicks}'" 11 | 12 | class Reports(db.Model): 13 | id = db.Column(db.Integer, primary_key=True) 14 | token = db.Column(db.String(16), index=True, nullable=False) 15 | message = db.Column(db.String(200), nullable=False) 16 | 17 | def __repr__(self): 18 | return f"'{self.id}' '{self.token}' '{self.message}'" 19 | -------------------------------------------------------------------------------- /tiny0/routes.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, redirect, url_for 2 | from tiny0 import app, db 3 | from tiny0.forms import URLForm, ShortURLForm, ReportForm 4 | from tiny0.models import URL, Reports 5 | from tiny0.token import gen_valid_token 6 | from tiny0.config import WEBSITE_DOMAIN 7 | 8 | # Index route 9 | @app.route("/", methods=['GET', 'POST']) 10 | def index(): 11 | # Create a instance of the form 12 | form = URLForm() 13 | 14 | # If the form was valid 15 | if form.validate_on_submit(): 16 | 17 | # If a token was given 18 | if form.token.data: 19 | # Add the token and the given url to the database 20 | db.session.add(URL(token=form.token.data, url=form.url.data)) 21 | db.session.commit() 22 | 23 | # Return the url page with the shortened url 24 | return render_template("url.html", url=WEBSITE_DOMAIN + "/" + form.token.data) 25 | 26 | # Else if a token was not given 27 | else: 28 | # Generate a valid token 29 | token = gen_valid_token() 30 | 31 | # Add the token and the given url to the database 32 | db.session.add(URL(token=token, url=form.url.data)) 33 | db.session.commit() 34 | 35 | # Return the url page with the shortened url 36 | return render_template("url.html", url=WEBSITE_DOMAIN + "/" + token) 37 | 38 | # Else if the form was invalid or not submitted 39 | else: 40 | # Return the index page with the form 41 | return render_template("index.html", form=form) 42 | 43 | # Shortened url route 44 | @app.route("/") 45 | def short_url(token): 46 | # Query the token in the database 47 | query = URL.query.filter_by(token=token).first() 48 | 49 | # If the query response was empty 50 | if not query: 51 | # Return the error page with a 404 not found error 52 | return render_template("error.html", error_code=404, error_message="Not Found"), 404 53 | 54 | # Else if the query response contained data 55 | else: 56 | # Addd one to the clicks of the shortened url 57 | query.clicks += 1 58 | db.session.commit() 59 | 60 | # Redirect to the url of the token 61 | return redirect(query.url) 62 | 63 | # Click tracker route 64 | @app.route("/tracker", methods=['GET', 'POST']) 65 | def tracker(): 66 | # Create a instance of the form 67 | form = ShortURLForm() 68 | 69 | # If the form was valid 70 | if form.validate_on_submit(): 71 | # Get the clicks of the given token 72 | clicks = URL.query.filter_by(token=form.url.data).first().clicks 73 | 74 | # Return the clicks page with the clicks of that token 75 | return render_template("clicks.html", clicks=clicks) 76 | 77 | # Else if the form was invalid or not submitted 78 | else: 79 | # Return the tracker page with the form 80 | return render_template("tracker.html", form=form) 81 | 82 | # url lookup route 83 | @app.route("/lookup", methods=['GET', 'POST']) 84 | def lookup(): 85 | # Create a instance of the form 86 | form = ShortURLForm() 87 | 88 | # If the form was valid 89 | if form.validate_on_submit(): 90 | # Get the original url of the given token 91 | url = URL.query.filter_by(token=form.url.data).first().url 92 | 93 | # Return the original url page with the url 94 | return render_template("original-url.html", url=url) 95 | 96 | # Else if the form was invalid or not submitted 97 | else: 98 | # Return the lookup page with the form 99 | return render_template("lookup.html", form=form) 100 | 101 | # url report route 102 | @app.route("/report", methods=['GET', 'POST']) 103 | def report(): 104 | # Create a instance of the form 105 | form = ReportForm() 106 | 107 | # If the form was valid 108 | if form.validate_on_submit(): 109 | # Add the report to the database 110 | db.session.add(Reports(token=form.url.data, message=form.message.data)) 111 | db.session.commit() 112 | 113 | # Return the thanks page 114 | return render_template("thanks.html") 115 | 116 | # Else if the form was invalid or not submitted 117 | else: 118 | # Return the report page with the form 119 | return render_template("report.html", form=form) 120 | 121 | # Donate route 122 | @app.route("/donate") 123 | def donate(): 124 | return render_template("donate.html") 125 | 126 | # Removed url route 127 | @app.route("/removed") 128 | def removed(): 129 | return render_template("removed.html") 130 | 131 | # Error handling routes 132 | @app.errorhandler(404) 133 | def error_404(error): 134 | return render_template("error.html", error_code=404, error_message="Not Found"), 404 135 | 136 | @app.errorhandler(500) 137 | def error_500(error): 138 | return render_template("error.html", error_code=500, error_message="Internal Server Error"), 500 139 | -------------------------------------------------------------------------------- /tiny0/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xemeds/tiny0/3a7b7e63a65edac4a4d6a168c0bd0ff682b83263/tiny0/static/favicon.ico -------------------------------------------------------------------------------- /tiny0/static/font.css: -------------------------------------------------------------------------------- 1 | /* Taken from: https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400&display=swap */ 2 | 3 | /* cyrillic-ext */ 4 | @font-face { 5 | font-family: 'Roboto Mono'; 6 | font-style: normal; 7 | font-weight: 400; 8 | font-display: swap; 9 | src: url(https://fonts.gstatic.com/s/robotomono/v12/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_SeW4Ep0.woff2) format('woff2'); 10 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 11 | } 12 | /* cyrillic */ 13 | @font-face { 14 | font-family: 'Roboto Mono'; 15 | font-style: normal; 16 | font-weight: 400; 17 | font-display: swap; 18 | src: url(https://fonts.gstatic.com/s/robotomono/v12/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_QOW4Ep0.woff2) format('woff2'); 19 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 20 | } 21 | /* greek */ 22 | @font-face { 23 | font-family: 'Roboto Mono'; 24 | font-style: normal; 25 | font-weight: 400; 26 | font-display: swap; 27 | src: url(https://fonts.gstatic.com/s/robotomono/v12/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_R-W4Ep0.woff2) format('woff2'); 28 | unicode-range: U+0370-03FF; 29 | } 30 | /* vietnamese */ 31 | @font-face { 32 | font-family: 'Roboto Mono'; 33 | font-style: normal; 34 | font-weight: 400; 35 | font-display: swap; 36 | src: url(https://fonts.gstatic.com/s/robotomono/v12/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_S-W4Ep0.woff2) format('woff2'); 37 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; 38 | } 39 | /* latin-ext */ 40 | @font-face { 41 | font-family: 'Roboto Mono'; 42 | font-style: normal; 43 | font-weight: 400; 44 | font-display: swap; 45 | src: url(https://fonts.gstatic.com/s/robotomono/v12/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_SuW4Ep0.woff2) format('woff2'); 46 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 47 | } 48 | /* latin */ 49 | @font-face { 50 | font-family: 'Roboto Mono'; 51 | font-style: normal; 52 | font-weight: 400; 53 | font-display: swap; 54 | src: url(https://fonts.gstatic.com/s/robotomono/v12/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_ROW4.woff2) format('woff2'); 55 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 56 | } 57 | -------------------------------------------------------------------------------- /tiny0/static/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: 'Robot Mono', monospace; 6 | } 7 | 8 | nav { 9 | background: #0c2431; 10 | padding: 5px 40px; 11 | } 12 | 13 | nav ul { 14 | list-style: none; 15 | display: flex; 16 | flex-wrap: wrap; 17 | align-items: center; 18 | justify-content: center; 19 | } 20 | 21 | nav ul li { 22 | padding: 15px 0; 23 | cursor: pointer; 24 | } 25 | 26 | nav ul li.items { 27 | position: relative; 28 | width: auto; 29 | margin: 0 16px; 30 | text-align: center; 31 | order: 3; 32 | font-weight: bold; 33 | } 34 | 35 | nav ul li.items:after { 36 | position: absolute; 37 | content: ''; 38 | left: 0; 39 | bottom: 5px; 40 | height: 2px; 41 | width: 100%; 42 | background: #f68741; 43 | opacity: 0; 44 | transition: all 0.2s linear; 45 | } 46 | 47 | nav ul li.items:hover:after { 48 | opacity: 1; 49 | bottom: 8px; 50 | } 51 | 52 | nav ul li.logo { 53 | flex: 1; 54 | color: white; 55 | font-size: 23px; 56 | font-weight: 600; 57 | cursor: default; 58 | user-select: none; 59 | } 60 | 61 | nav ul li a { 62 | color: white; 63 | font-size: 18px; 64 | text-decoration: none; 65 | transition: .4s; 66 | } 67 | 68 | nav ul li:hover a { 69 | color: #f68741; 70 | } 71 | 72 | nav ul li i { 73 | font-size: 23px; 74 | } 75 | 76 | nav ul li.btn { 77 | display: none; 78 | } 79 | 80 | nav ul li.btn.hide i:before { 81 | content: '\f00d'; 82 | } 83 | 84 | @media all and (max-width: 900px) { 85 | nav { 86 | padding: 5px 30px; 87 | } 88 | nav ul li.items { 89 | width: 100%; 90 | display: none; 91 | } 92 | nav ul li.items.show { 93 | display: block; 94 | } 95 | nav ul li.btn { 96 | display: block; 97 | } 98 | nav ul li.items:hover { 99 | border-radius: 5px; 100 | box-shadow: 0px 5px 5px #f68741, 0px -5px 5px #f68741; 101 | } 102 | nav ul li.items:hover:after { 103 | opacity: 0; 104 | } 105 | } 106 | 107 | body { 108 | background: #0c2431; 109 | } 110 | 111 | .url-form { 112 | max-width: 700px; 113 | text-align: center; 114 | margin: 20vh auto 0 auto; 115 | } 116 | 117 | .url-form h1 { 118 | color: #ffffff; 119 | text-align: center; 120 | margin-bottom: 30px; 121 | } 122 | 123 | .feedback-input { 124 | color: white; 125 | font-weight: 500; 126 | font-size: 18px; 127 | border-radius: 5px; 128 | line-height: 22px; 129 | background-color: transparent; 130 | border: 2px solid #fc913a; 131 | transition: all 0.3s; 132 | padding: 13px; 133 | margin-bottom: 15px; 134 | width: 100%; 135 | box-sizing: border-box; 136 | outline: 0; 137 | } 138 | 139 | .feedback-input:focus { 140 | border: 2px solid #c95c03; 141 | } 142 | 143 | .token { 144 | display: flex; 145 | } 146 | 147 | .token h3 { 148 | color: #ffffff; 149 | margin: 15px 10px 0 0; 150 | } 151 | 152 | .button { 153 | width: 100%; 154 | background: #fc913a; 155 | border-radius: 5px; 156 | border: 0; 157 | cursor: pointer; 158 | color: white; 159 | font-size: 24px; 160 | padding-top: 10px; 161 | padding-bottom: 10px; 162 | transition: all 0.3s; 163 | margin-top: -4px; 164 | font-weight: 700; 165 | } 166 | 167 | .button:hover { 168 | background: #e26803; 169 | } 170 | 171 | @media all and (max-width: 800px) { 172 | .url-form { 173 | max-width: 380px; 174 | } 175 | } 176 | 177 | .form-errors { 178 | color: #ffffff; 179 | text-align: center; 180 | margin-top: 15px; 181 | } 182 | 183 | .form-errors h4 { 184 | margin-bottom: 5px; 185 | } 186 | 187 | .clicks { 188 | color: #f68741; 189 | text-align: center; 190 | margin-top: 20vh; 191 | } 192 | 193 | .clicks h1 { 194 | font-size: 125px; 195 | } 196 | 197 | .clicks h2 { 198 | font-size: 25px; 199 | } 200 | 201 | .thanks { 202 | color: #f68741; 203 | text-align: center; 204 | margin-top: 20vh; 205 | } 206 | 207 | .donation { 208 | color: #ffffff; 209 | text-align: center; 210 | margin-top: 20vh; 211 | } 212 | 213 | .donation h1 { 214 | font-size: 50px; 215 | margin-bottom: 30px; 216 | } 217 | 218 | .donation h2 { 219 | font-size: 30px; 220 | margin-bottom: 60px; 221 | } 222 | 223 | .donation h2 a:link { 224 | color: #f68741; 225 | text-decoration: none; 226 | } 227 | 228 | .donation h2 a:visited { 229 | color: #f68741; 230 | } 231 | 232 | .donation h2 a:hover { 233 | color: #e26803; 234 | } 235 | 236 | .removed { 237 | color: #f68741; 238 | text-align: center; 239 | margin-top: 250px; 240 | } 241 | 242 | .removed h1 { 243 | font-size: 50px; 244 | } 245 | 246 | .error { 247 | color: #ffffff; 248 | text-align: center; 249 | margin-top: 250px; 250 | } 251 | 252 | .error h1 { 253 | font-size: 100px; 254 | } 255 | 256 | .error h2 { 257 | font-size: 50px; 258 | } 259 | 260 | footer { 261 | color: #ffffff; 262 | text-align: center; 263 | margin: 30vh 0 5vh 0; 264 | } 265 | -------------------------------------------------------------------------------- /tiny0/templates/clicks.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
5 |

{{ clicks }}

6 |

Click(s)

7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /tiny0/templates/donate.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
5 |

Patreon

6 |

patreon.com/xemeds

7 |

Bitcoin

8 |

1Mg55rPVuQ2P8zKsCcLdsmgqH24uLXfLbR

9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /tiny0/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
5 |

{{ error_code }}

6 |

{{ error_message }}

7 |
8 | {% endblock %} -------------------------------------------------------------------------------- /tiny0/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
5 |

< URL Shortener >

6 | {{ form.hidden_tag() }} 7 | {{ form.url(placeholder="Enter the URL here", autofocus=true, class="feedback-input") }} 8 |
9 |

tiny0.cc/

10 | {{ form.token(placeholder="Use custom alias (optional)", class="feedback-input") }} 11 |
12 | {{ form.submit(class="button") }} 13 | {% if form.errors %} 14 |
15 | {% for error in form.url.errors %} 16 |

{{ error }}

17 | {% endfor %} 18 | {% for error in form.token.errors %} 19 |

{{ error }}

20 | {% endfor %} 21 |
22 | {% endif %} 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /tiny0/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | tiny0 - Custom URL Shortener 17 | 18 | 19 | 30 | 31 | {% block body %}{% endblock %} 32 | 33 | 42 | 43 | 44 | 47 | 48 | -------------------------------------------------------------------------------- /tiny0/templates/lookup.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
5 |

< URL Lookup >

6 | {{ form.hidden_tag() }} 7 | {{ form.url(placeholder="Enter the short URL here", autofocus=true, class="feedback-input") }} 8 | {{ form.submit(class="button") }} 9 | {% if form.errors %} 10 |
11 | {% for error in form.url.errors %} 12 |

{{ error }}

13 | {% endfor %} 14 |
15 | {% endif %} 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /tiny0/templates/original-url.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
5 |

< Original URL >

6 | 7 | 8 |
9 | {% endblock %} 10 | 11 | {% block script %} 12 | function copyURL() { 13 | var url = document.getElementById("url"); 14 | url.select(); 15 | url.setSelectionRange(0, 99999) 16 | document.execCommand("copy"); 17 | } 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /tiny0/templates/removed.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
5 |

URL removed

6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /tiny0/templates/report.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
5 |

< Report >

6 | {{ form.hidden_tag() }} 7 | {{ form.url(placeholder="Enter the short URL", autofocus=true, class="feedback-input") }} 8 | {{ form.message(placeholder="Enter a short message explaining the reason of your report", class="feedback-input") }} 9 | {{ form.submit(class="button") }} 10 | {% if form.errors %} 11 |
12 | {% for error in form.url.errors %} 13 |

{{ error }}

14 | {% endfor %} 15 | {% for error in form.message.errors %} 16 |

{{ error }}

17 | {% endfor %} 18 |
19 | {% endif %} 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /tiny0/templates/thanks.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
5 |

Your report has been submitted

6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /tiny0/templates/tracker.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
5 |

< Click Tracker >

6 | {{ form.hidden_tag() }} 7 | {{ form.url(placeholder="Enter the short URL here", autofocus=true, class="feedback-input") }} 8 | {{ form.submit(class="button") }} 9 | {% if form.errors %} 10 |
11 | {% for error in form.url.errors %} 12 |

{{ error }}

13 | {% endfor %} 14 |
15 | {% endif %} 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /tiny0/templates/url.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block body %} 4 |
5 |

< Short URL >

6 | 7 | 8 |
9 | {% endblock %} 10 | 11 | {% block script %} 12 | function copyURL() { 13 | var url = document.getElementById("url"); 14 | url.select(); 15 | url.setSelectionRange(0, 99999) 16 | document.execCommand("copy"); 17 | } 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /tiny0/token.py: -------------------------------------------------------------------------------- 1 | from tiny0 import db 2 | from tiny0.models import URL 3 | from secrets import choice 4 | 5 | # The characters used to generate the token 6 | token_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_-" 7 | 8 | def gen_valid_token(): 9 | while True: 10 | # Generate a token 11 | token = "".join(choice(token_characters) for i in range(6)) 12 | 13 | # If the token does not exists in the database 14 | if not db.session.query(db.session.query(URL).filter_by(token=token).exists()).scalar(): 15 | # Break the loop 16 | break 17 | 18 | # Return the token 19 | return token 20 | 21 | --------------------------------------------------------------------------------