mirror of
https://github.com/kevin-DL/ShortMe-URL-Shortener.git
synced 2026-01-16 05:04:39 +00:00
Initial commit
This commit is contained in:
0
app/server/__init__.py
Normal file
0
app/server/__init__.py
Normal file
0
app/server/api/__init__.py
Normal file
0
app/server/api/__init__.py
Normal file
112
app/server/api/api.py
Normal file
112
app/server/api/api.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# ------- standard library imports -------
|
||||
import requests
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
from flask_restful import Resource, reqparse
|
||||
|
||||
# ------- local imports -------
|
||||
from app.server.db.extensions import db
|
||||
from app.server.api.api_auth import auth
|
||||
from app.server.db.models import Url, AuthToken
|
||||
|
||||
shorten_parser = reqparse.RequestParser()
|
||||
total_clicks_parser = reqparse.RequestParser()
|
||||
|
||||
shorten_parser.add_argument('url', type=str, help='URL parameter is missing', required=True)
|
||||
total_clicks_parser.add_argument('url', type=str, help='Short URL is missing', required=True)
|
||||
|
||||
|
||||
class Shorten(Resource):
|
||||
"""
|
||||
Return a short URL from a given URL
|
||||
URL: /api/shorten
|
||||
METHOD: POST
|
||||
PARAMS: url: long URL
|
||||
HEADERS: Authorization: (Bearer <key>)
|
||||
RETURN: dictionary with the short URL
|
||||
"""
|
||||
|
||||
decorators = [auth.login_required]
|
||||
|
||||
@staticmethod
|
||||
def post():
|
||||
args = shorten_parser.parse_args()
|
||||
url = args['url']
|
||||
original_url = url if url.startswith('http') else ('http://' + url)
|
||||
|
||||
try:
|
||||
res = requests.get(original_url)
|
||||
|
||||
if res.status_code == 200:
|
||||
url = Url(original_url=original_url)
|
||||
|
||||
db.session.add(url)
|
||||
db.session.commit()
|
||||
|
||||
return dict(
|
||||
short_url=url.short_url,
|
||||
original_url=url.original_url,
|
||||
success=True
|
||||
), 200
|
||||
|
||||
else:
|
||||
"""in case of page_not_found response from the URL given"""
|
||||
return dict(
|
||||
success=False,
|
||||
message='could not shorten this URL (page_not_found)'
|
||||
), 404
|
||||
|
||||
except (requests.exceptions.MissingSchema, requests.exceptions.ConnectionError, requests.exceptions.InvalidURL):
|
||||
return dict(
|
||||
success=False,
|
||||
message='could not shorten this URL (page_not_found)'
|
||||
), 404
|
||||
|
||||
|
||||
class TotalClicks(Resource):
|
||||
"""
|
||||
return the total clicks of a short URL
|
||||
URL: /api/total_clicks
|
||||
METHOD: GET
|
||||
PARAMS: short url
|
||||
RETURN: dictionary with the total short url visits.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get():
|
||||
args = total_clicks_parser.parse_args()
|
||||
url = args['url'].split('/')[-1]
|
||||
|
||||
try:
|
||||
url = Url.query.filter_by(short_url=url).first()
|
||||
|
||||
return dict(
|
||||
total=url.visits,
|
||||
short_url=url.short_url,
|
||||
original_url=url.original_url,
|
||||
success=True
|
||||
), 200
|
||||
|
||||
except AttributeError:
|
||||
return dict(
|
||||
success=False,
|
||||
message='could not find the URL (page_not_found)'
|
||||
), 404
|
||||
|
||||
|
||||
class GetToken(Resource):
|
||||
"""
|
||||
Return a unique API authorization token. Used only internaly by the web app.
|
||||
URL: /api/get_token
|
||||
METHOD: GET
|
||||
RETURN: unique API authorization token
|
||||
AuthToken: Required
|
||||
"""
|
||||
decorators = [auth.login_required]
|
||||
|
||||
@staticmethod
|
||||
def get():
|
||||
token = AuthToken()
|
||||
db.session.add(token)
|
||||
db.session.commit()
|
||||
return str(token)
|
||||
12
app/server/api/api_auth.py
Normal file
12
app/server/api/api_auth.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from flask_httpauth import HTTPTokenAuth
|
||||
|
||||
from app.server.db.models import AuthToken
|
||||
|
||||
auth = HTTPTokenAuth(scheme='Bearer')
|
||||
|
||||
|
||||
@auth.verify_token
|
||||
def verify_token(token):
|
||||
"""Used to verify user's Token"""
|
||||
return AuthToken.query.filter_by(auth_token=token).first()
|
||||
|
||||
0
app/server/db/__init__.py
Normal file
0
app/server/db/__init__.py
Normal file
4
app/server/db/extensions.py
Normal file
4
app/server/db/extensions.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
87
app/server/db/models.py
Normal file
87
app/server/db/models.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# ------- standard library imports -------
|
||||
import random
|
||||
import string
|
||||
import secrets
|
||||
from random import choices
|
||||
from datetime import datetime
|
||||
|
||||
# ------- local imports -------
|
||||
from sqlalchemy import ForeignKey
|
||||
from .extensions import db
|
||||
|
||||
|
||||
class Url(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
original_url = db.Column(db.String(512))
|
||||
short_url = db.Column(db.String(5), unique=True)
|
||||
visits = db.Column(db.Integer, default=0)
|
||||
date_created = db.Column(db.DateTime, default=datetime.now)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.short_url = self.generate_short_url()
|
||||
|
||||
def generate_short_url(self):
|
||||
characters = string.digits + string.ascii_letters
|
||||
short_url = ''.join(choices(characters, k=5))
|
||||
url = self.query.filter_by(short_url=short_url).first()
|
||||
|
||||
if url:
|
||||
return self.generate_short_url()
|
||||
|
||||
return short_url
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.original_url}, {self.visits}'
|
||||
|
||||
|
||||
class AuthToken(db.Model):
|
||||
__tablename__ = 'auth_token'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
auth_token = db.Column(db.String(16))
|
||||
date_created = db.Column(db.DateTime, default=datetime.now)
|
||||
email_id = db.Column(db.Integer, ForeignKey('email.id'))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.auth_token = self.generate_auth_token() if self.auth_token is None else self.auth_token
|
||||
|
||||
@staticmethod
|
||||
def generate_auth_token():
|
||||
generated_token = secrets.token_hex(16)
|
||||
return generated_token
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.auth_token}'
|
||||
|
||||
|
||||
class Email(db.Model):
|
||||
__tablename__ = 'email'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(512), unique=True)
|
||||
date_created = db.Column(db.DateTime, default=datetime.now)
|
||||
is_verified = db.Column(db.Boolean, default=False)
|
||||
verification_code = db.relationship('VerificationCode', backref='email')
|
||||
auth_token = db.relationship('AuthToken', backref='email')
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.email}, {self.verification_code}, {self.auth_token}'
|
||||
|
||||
|
||||
class VerificationCode(db.Model):
|
||||
__tablename__ = 'verification_code'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email_id = db.Column(db.Integer, ForeignKey('email.id'))
|
||||
verification_code = db.Column(db.String, unique=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.verification_code = self.generate_verification_code()
|
||||
|
||||
@staticmethod
|
||||
def generate_verification_code():
|
||||
verification_code = ' '.join([str(random.randint(0, 999)).zfill(3) for _ in range(2)])
|
||||
return verification_code
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.verification_code}'
|
||||
9
app/server/routes/api_doc.py
Normal file
9
app/server/routes/api_doc.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
api_doc_blueprint = Blueprint('api_doc_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@api_doc_blueprint.route('/api_doc')
|
||||
def api_doc():
|
||||
return render_template('api_doc.html')
|
||||
9
app/server/routes/error.py
Normal file
9
app/server/routes/error.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
error_blueprint = Blueprint('error_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@error_blueprint.route('/error')
|
||||
def error():
|
||||
return render_template('error.html')
|
||||
9
app/server/routes/get_token.py
Normal file
9
app/server/routes/get_token.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
get_token_blueprint = Blueprint('get_token_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@get_token_blueprint.route('/get_token')
|
||||
def get_token():
|
||||
return render_template('get_token.html')
|
||||
9
app/server/routes/index.py
Normal file
9
app/server/routes/index.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import render_template, Blueprint
|
||||
|
||||
index_blueprint = Blueprint('index_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@index_blueprint.route("/")
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
0
app/server/routes/internal/__init__.py
Normal file
0
app/server/routes/internal/__init__.py
Normal file
13
app/server/routes/internal/favicon.py
Normal file
13
app/server/routes/internal/favicon.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# ------- standard library imports -------
|
||||
import os
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, send_from_directory
|
||||
|
||||
app_blueprint = Blueprint('app_blueprint', __name__)
|
||||
|
||||
|
||||
@app_blueprint.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(app_blueprint.root_path, 'static'),
|
||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
24
app/server/routes/internal/redirect_to_url.py
Normal file
24
app/server/routes/internal/redirect_to_url.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, redirect, url_for
|
||||
|
||||
# ------- local imports -------
|
||||
from app.server.db.models import Url
|
||||
from app.server.db.extensions import db
|
||||
|
||||
redirect_to_url_blueprint = Blueprint('redirect_to_url_blueprint', __name__)
|
||||
|
||||
|
||||
@redirect_to_url_blueprint.route('/<short_url>')
|
||||
def redirect_to_url(short_url):
|
||||
"""
|
||||
This function will query the database with the short_url and will
|
||||
redirect to the original url if it exist in the database.
|
||||
"""
|
||||
url = Url.query.filter_by(short_url=short_url).first()
|
||||
|
||||
if url:
|
||||
url.visits = url.visits + 1
|
||||
db.session.commit()
|
||||
return redirect(url.original_url)
|
||||
|
||||
return redirect(url_for('page_not_found_blueprint.page_not_found'))
|
||||
50
app/server/routes/internal/send_verification_code.py
Normal file
50
app/server/routes/internal/send_verification_code.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# ------- standard library imports -------
|
||||
import re
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
from flask_mail import Mail, Message
|
||||
from flask import Blueprint, request, redirect, url_for, session
|
||||
|
||||
# ------- local imports -------
|
||||
from app import app
|
||||
from app.server.db.extensions import db
|
||||
from app.server.db.models import VerificationCode, Email
|
||||
|
||||
send_otp_blueprint = Blueprint('send_otp_blueprint', __name__)
|
||||
|
||||
|
||||
@send_otp_blueprint.route('/email_validation', methods=['POST'])
|
||||
def email_validation():
|
||||
input_email = request.form['email']
|
||||
pattern = r'[^@]+@[^@]+\.[^@]+'
|
||||
is_email_valid = re.match(pattern, input_email)
|
||||
|
||||
if is_email_valid:
|
||||
email = Email.query.filter_by(email=input_email).first()
|
||||
|
||||
if email and email.is_verified:
|
||||
auth_token = Email.query.filter_by(email=input_email).first().auth_token
|
||||
return redirect(url_for('your_api_token_blueprint.your_api_token', auth_token=auth_token))
|
||||
|
||||
elif email and not email.is_verified:
|
||||
return redirect(url_for('verify_code_blueprint.enter_verification_code', is_verified=False))
|
||||
|
||||
else:
|
||||
if not email:
|
||||
session['user_email'] = input_email
|
||||
|
||||
email = Email(email=input_email)
|
||||
verification_code = VerificationCode(email=email)
|
||||
db.session.add(verification_code, email)
|
||||
db.session.commit()
|
||||
|
||||
# print('user added to db')
|
||||
# print('verification_code:')
|
||||
# print(verification_code)
|
||||
# print('*' * 33)
|
||||
|
||||
mail = Mail(app.app)
|
||||
msg = Message('ShortMe Verification Code', sender='shortme.biz@gmail.com', recipients=[email.email])
|
||||
msg.body = str(f'Hi!\nThis is your verification code: {verification_code.verification_code}')
|
||||
mail.send(msg)
|
||||
return redirect(url_for('verify_code_blueprint.enter_verification_code'))
|
||||
40
app/server/routes/internal/shorten_url.py
Normal file
40
app/server/routes/internal/shorten_url.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# ------- standard library imports -------
|
||||
import json
|
||||
import requests
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
import flask
|
||||
from flask import Blueprint, request, redirect, url_for
|
||||
|
||||
# ------- local imports -------
|
||||
from app import app
|
||||
|
||||
shorten_url_blueprint = Blueprint('shorten_url_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@shorten_url_blueprint.route('/shorten', methods=['POST'])
|
||||
def shorten_url():
|
||||
base_url = flask.url_for("index_blueprint.index", _external=True)
|
||||
original_url = request.form['original_url']
|
||||
shorten_endpoint = base_url + 'api/shorten'
|
||||
|
||||
params = {
|
||||
'url': original_url
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {app.app.secret_key}'
|
||||
}
|
||||
|
||||
response = requests.post(shorten_endpoint, headers=headers, params=params)
|
||||
|
||||
if response.status_code == 200:
|
||||
response = json.loads(response.text)
|
||||
short_url = response['short_url']
|
||||
original_url = response['original_url']
|
||||
|
||||
return redirect(url_for('your_short_url_blueprint.your_short_url',
|
||||
short_url=short_url,
|
||||
original_url=original_url))
|
||||
else:
|
||||
return redirect(url_for('error_blueprint.error'))
|
||||
9
app/server/routes/page_not_found.py
Normal file
9
app/server/routes/page_not_found.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
page_not_found_blueprint = Blueprint('page_not_found_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@page_not_found_blueprint.route('/page_not_found')
|
||||
def page_not_found():
|
||||
return render_template('404.html')
|
||||
24
app/server/routes/total_clicks.py
Normal file
24
app/server/routes/total_clicks.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# ------- standard library imports -------
|
||||
import json
|
||||
import requests
|
||||
|
||||
# ------- 3rd party imports -------
|
||||
import flask
|
||||
from flask import Blueprint, render_template, request
|
||||
|
||||
total_clicks_blueprint = Blueprint('total_clicks_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@total_clicks_blueprint.route('/total_clicks')
|
||||
def total_clicks():
|
||||
short_url = request.args['short_url']
|
||||
base_url = flask.url_for("index_blueprint.index", _external=True)
|
||||
total_clicks_endpoint = base_url + 'api/total_clicks'
|
||||
|
||||
params = {
|
||||
'url': short_url
|
||||
}
|
||||
|
||||
response = requests.get(total_clicks_endpoint, params=params)
|
||||
total_url_clicks = json.loads(response.text)['total']
|
||||
return render_template('total-clicks.html', total_clicks=total_url_clicks)
|
||||
32
app/server/routes/verify_code.py
Normal file
32
app/server/routes/verify_code.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, session
|
||||
|
||||
# ------- local imports -------
|
||||
from app.server.db.extensions import db
|
||||
from app.server.db.models import VerificationCode, AuthToken, Email
|
||||
|
||||
verify_code_blueprint = Blueprint('verify_code_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@verify_code_blueprint.route('/verify', methods=['GET', 'POST'])
|
||||
def enter_verification_code():
|
||||
is_verified = request.args.get('is_verified')
|
||||
is_code_valid = request.args.get('is_code_valid')
|
||||
return render_template('verify.html', is_code_valid=is_code_valid, is_verified=is_verified)
|
||||
|
||||
|
||||
@verify_code_blueprint.route('/validate_code', methods=['POST'])
|
||||
def validate_code():
|
||||
input_code = request.form['verification']
|
||||
code = VerificationCode.query.filter_by(verification_code=input_code).first()
|
||||
|
||||
if code:
|
||||
email = Email.query.filter_by(email=session['user_email']).first()
|
||||
email.is_verified = True
|
||||
auth_token = AuthToken(email=email)
|
||||
db.session.add(auth_token)
|
||||
db.session.commit()
|
||||
return redirect(url_for('your_api_token_blueprint.your_api_token', auth_token=auth_token))
|
||||
|
||||
else:
|
||||
return redirect(url_for('verify_code_blueprint.enter_verification_code', is_code_valid=False))
|
||||
15
app/server/routes/your_api_token.py
Normal file
15
app/server/routes/your_api_token.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# ------- 3rd party imports -------
|
||||
from flask import Blueprint, render_template, request, url_for
|
||||
|
||||
your_api_token_blueprint = Blueprint('your_api_token_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@your_api_token_blueprint.route('/your_api_token')
|
||||
def your_api_token():
|
||||
auth_token = request.args.get("auth_token")
|
||||
|
||||
if auth_token:
|
||||
return render_template('your_api_token.html', auth_token=auth_token)
|
||||
|
||||
else:
|
||||
return render_template(url_for('page_not_found_blueprint.page_not_found'))
|
||||
19
app/server/routes/your_short_url.py
Normal file
19
app/server/routes/your_short_url.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# ------- 3rd party imports -------
|
||||
import flask
|
||||
from flask import Blueprint, render_template, request
|
||||
|
||||
your_short_url_blueprint = Blueprint('your_short_url_blueprint', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@your_short_url_blueprint.route('/your_short_url')
|
||||
def your_short_url():
|
||||
original_url = request.args['original_url']
|
||||
short_url = request.args['short_url']
|
||||
base_url = flask.url_for("index_blueprint.index", _external=True)
|
||||
full_short_url = f'{base_url}{short_url}'
|
||||
full_short_url = full_short_url.replace('http://www.', '')
|
||||
|
||||
return render_template('your_short_url.html',
|
||||
original_url=original_url,
|
||||
short_url=short_url,
|
||||
full_short_url=full_short_url)
|
||||
Reference in New Issue
Block a user