├── .gitignore ├── README.md ├── app ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-37.pyc │ ├── config.cpython-37.pyc │ ├── error_codes.cpython-37.pyc │ ├── models.cpython-37.pyc │ └── validators.cpython-37.pyc ├── auth │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-37.pyc │ │ └── routes.cpython-37.pyc │ └── routes.py ├── config.py ├── error_codes.py ├── links │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-37.pyc │ │ └── routes.cpython-37.pyc │ └── routes.py ├── models.py ├── site.db ├── users │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-37.pyc │ │ └── routes.cpython-37.pyc │ └── routes.py ├── utils │ ├── __pycache__ │ │ ├── classes.cpython-37.pyc │ │ ├── decorators.cpython-37.pyc │ │ └── functions.cpython-37.pyc │ ├── classes.py │ ├── decorators.py │ └── functions.py └── validators.py ├── doc.png ├── manager.py ├── requirements.txt └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | bin/ 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | include/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linktree-API 2 | A simple implementation of the Linktree-API 3 | 4 | Documentation on postman: [https://documenter.getpostman.com/view/14039622/Tzsik4P8](https://documenter.getpostman.com/view/14039622/Tzsik4P8) 5 | 6 | ![simple doc image](doc.png) 7 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | from flask_restful import Api 4 | from flask_bcrypt import Bcrypt 5 | from app.config import Config 6 | 7 | from flask_script import Manager 8 | from flask_migrate import Migrate, MigrateCommand 9 | 10 | app = Flask(__name__) 11 | app.config.from_object(Config) # adding Config 12 | 13 | db = SQLAlchemy(app) 14 | api = Api(app) 15 | bcrypt = Bcrypt(app) 16 | migrate = Migrate(app, db) 17 | manager = Manager(app) 18 | manager.add_command("db", MigrateCommand) 19 | 20 | 21 | 22 | from app.auth import * 23 | from app.users import * 24 | from app.links import * 25 | 26 | from app.models import * 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/__pycache__/config.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/__pycache__/config.cpython-37.pyc -------------------------------------------------------------------------------- /app/__pycache__/error_codes.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/__pycache__/error_codes.cpython-37.pyc -------------------------------------------------------------------------------- /app/__pycache__/models.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/__pycache__/models.cpython-37.pyc -------------------------------------------------------------------------------- /app/__pycache__/validators.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/__pycache__/validators.cpython-37.pyc -------------------------------------------------------------------------------- /app/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from app.auth.routes import Login, SignUp 2 | from app import api 3 | 4 | api.add_resource(Login, "/auth/login") 5 | api.add_resource(SignUp, "/auth/signup") 6 | -------------------------------------------------------------------------------- /app/auth/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/auth/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/auth/__pycache__/routes.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/auth/__pycache__/routes.cpython-37.pyc -------------------------------------------------------------------------------- /app/auth/routes.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | request, 3 | current_app 4 | ) 5 | from flask_restful import Resource 6 | from app.utils.functions import JSONResponse 7 | from app.utils.decorators import args_check 8 | from app.validators import LoginValidator, SignupValidator 9 | from app.models import User 10 | 11 | from app.error_codes import ( 12 | E003, # Username Already Exists 13 | E004, # Email Already Exists 14 | E011, # Authentication Error 15 | E012, # SignUp Error 16 | E013, # Login Error 17 | E021 # User does not exists 18 | ) 19 | 20 | 21 | class Login(Resource): 22 | 23 | @args_check(LoginValidator()) 24 | def post(self, json_data): 25 | """Creates an **authorization token** for a user if successfully 26 | logged in 27 | 28 | JSON parameters: 29 | :param email: User's email 30 | :type email: String 31 | :param password: User's password 32 | :type password: String 33 | 34 | :return: JSON Object with a token value 35 | 36 | Reference: 37 | /validators.py 38 | """ 39 | 40 | with current_app.app_context(): 41 | email = json_data.email.strip().lower() 42 | password = json_data.password 43 | 44 | # Checks if User with `email` exists 45 | user = User.query.filter_by(email=email).first() 46 | 47 | if not user: 48 | return JSONResponse(code=E021) 49 | 50 | if not user.password_is_valid(password): 51 | 52 | # E013 = Login Error 53 | return JSONResponse( 54 | message="Incorrect username or password!", 55 | code=E013 56 | ) 57 | 58 | token = user.generate_token() 59 | 60 | return JSONResponse(data={ 'token' : token, "user": user.to_dict() }) 61 | 62 | return JSONResponse( 63 | message="Error logging in this account", 64 | code=E014 65 | ) 66 | 67 | class SignUp(Resource): 68 | 69 | @args_check(SignupValidator()) 70 | def post(self, json_data): 71 | """Creates an **authorization token** for a user if successfully 72 | signed up 73 | 74 | JSON parameters: 75 | :param username: User's username 76 | :type username: String 77 | :param email: User's email 78 | :type email: String 79 | :param password: User's password 80 | :type password: String 81 | 82 | :return: JSON Object with a token value 83 | 84 | Reference: 85 | /validators.py 86 | """ 87 | 88 | with current_app.app_context(): 89 | username = json_data.username.strip().lower() 90 | email = json_data.email.strip().lower() 91 | password = json_data.password 92 | 93 | # Checks if User with `email` exists 94 | existing_user = User.query.filter_by(email=email).first() 95 | 96 | if existing_user: 97 | return JSONResponse( 98 | message="User with this email already exists!", 99 | code=E004 100 | ) 101 | 102 | # Checks if User with `username` exists 103 | existing_user = User.query.filter_by(username=username).first() 104 | 105 | if existing_user: 106 | return JSONResponse( 107 | message="User with this username already exists!", 108 | code=E003 109 | ) 110 | 111 | user = User( 112 | username = username, 113 | email = email 114 | ) 115 | 116 | # Hashing password 117 | user.set_password(password) 118 | user.save() 119 | 120 | # Setting public ID 121 | user.set_public_id() 122 | 123 | token = user.generate_token() 124 | 125 | return JSONResponse(data={ 'token' : token, "user": user.to_dict() }) 126 | 127 | return JSONResponse( 128 | message="Error creating an account", 129 | code=E014 130 | ) -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | class Config: 2 | SECRET_KEY = 'secret' 3 | SQLALCHEMY_DATABASE_URI = 'sqlite:///site.db' 4 | DEBUG = True # some Flask specific configs 5 | -------------------------------------------------------------------------------- /app/error_codes.py: -------------------------------------------------------------------------------- 1 | # STATUS CODES 2 | SUCCESS = 200 3 | SUCCESS_CREATED = 201 4 | SUCCESS_NO_CONTENT = 204 5 | NOT_MODIFIED = 304 6 | BAD_REQUEST = 400 7 | INVALID_PARAMETERS = BAD_REQUEST 8 | UNAUTHORIZED = 401 9 | NO_PERMISSION = 401 10 | FORBIDDEN = 403 11 | NOT_FOUND = 404 12 | CONFLICT = 409 13 | INTERNAL_SERVER_ERROR = 500 14 | FILE_TOO_LARGE = 400 15 | 16 | 17 | # Validation Errors 18 | E001 = "E001" 19 | E002 = "E002" 20 | E003 = "E003" 21 | E004 = "E004" 22 | E005 = "E005" 23 | E006 = "E006" 24 | E007 = "E007" 25 | E008 = "E008" 26 | E009 = "E009" 27 | 28 | # Authentication 29 | E011 = "E011" 30 | E012 = "E012" 31 | E013 = "E013" 32 | E014 = "E014" 33 | E015 = "E015" 34 | 35 | # User Errors 36 | E021 = "E021" 37 | E022 = "E022" 38 | E023 = "E023" 39 | E024 = "E024" 40 | E025 = "E025" 41 | 42 | # Link Errors 43 | E031 = "E031" 44 | E032 = "E032" 45 | E033 = "E033" 46 | E034 = "E034" 47 | E035 = "E035" 48 | E036 = "E036" 49 | 50 | # MISC 51 | E041 = "E041" 52 | E042 = "E042" 53 | E043 = "E043" 54 | E044 = "E044" 55 | E045 = "E045" 56 | 57 | ERRORS_DESCRIPTION = dict( 58 | E001 = "Insufficient Parameters", 59 | E002 = "Invalid Request JSON", 60 | E003 = "Username Already Exists", 61 | E004 = "Email Already Exists", 62 | E005 = "Exceeded Maximum Number Of Uploads", 63 | E006 = "E006", 64 | E007 = "E007", 65 | E008 = "E008", 66 | E009 = "E009", 67 | 68 | E011 = "Authentication Error", 69 | E012 = "SignUp Error", 70 | E013 = "Login Error", 71 | E014 = "Error Authenticating User", 72 | E015 = "E015", 73 | 74 | E021 = "User does not exists", 75 | E022 = "Cannot delete this user", 76 | E023 = "Cannot edit this user", 77 | E024 = "Error editing user", 78 | E025 = "Error deleting user", 79 | 80 | E031 = "Link does not exists", 81 | E032 = "User Cannot delete this link", 82 | E033 = "User Cannot edit this link", 83 | E034 = "Error editing link", 84 | E035 = "Error deleting link", 85 | E036 = "Error Adding Link", 86 | 87 | E041 = "Image Not Found", 88 | E042 = "E042", 89 | E043 = "E043", 90 | E044 = "E044", 91 | E045 = "E045", 92 | ) 93 | 94 | 95 | 96 | ERRORS_STATUS_CODE = dict( 97 | E001 = BAD_REQUEST, 98 | E002 = BAD_REQUEST, 99 | E003 = BAD_REQUEST, 100 | E004 = BAD_REQUEST, 101 | E005 = BAD_REQUEST, 102 | E006 = "E006", 103 | E007 = "E007", 104 | E008 = "E008", 105 | E009 = "E009", 106 | 107 | E011 = BAD_REQUEST, 108 | E012 = BAD_REQUEST, 109 | E013 = BAD_REQUEST, 110 | E014 = BAD_REQUEST, 111 | E015 = BAD_REQUEST, 112 | 113 | E021 = NOT_FOUND, 114 | E022 = NO_PERMISSION, 115 | E023 = NO_PERMISSION, 116 | E024 = INTERNAL_SERVER_ERROR, 117 | E025 = INTERNAL_SERVER_ERROR, 118 | 119 | E031 = NOT_FOUND, 120 | E032 = NO_PERMISSION, 121 | E033 = NO_PERMISSION, 122 | E034 = BAD_REQUEST, 123 | E035 = BAD_REQUEST, 124 | E036 = BAD_REQUEST, 125 | 126 | E041 = NOT_FOUND, 127 | E042 = "E042", 128 | E043 = "E043", 129 | E044 = "E044", 130 | E045 = "E045", 131 | ) 132 | -------------------------------------------------------------------------------- /app/links/__init__.py: -------------------------------------------------------------------------------- 1 | from app.links.routes import Links 2 | from app import api 3 | 4 | api.add_resource(Links, "/links/","/links") 5 | -------------------------------------------------------------------------------- /app/links/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/links/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/links/__pycache__/routes.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/links/__pycache__/routes.cpython-37.pyc -------------------------------------------------------------------------------- /app/links/routes.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | request, 3 | current_app 4 | ) 5 | from flask_restful import Resource 6 | from app.utils.functions import JSONResponse 7 | from app.utils.decorators import args_check, login_required 8 | from app.models import Link 9 | from app.validators import LinkValidator 10 | 11 | from app.error_codes import ( 12 | E031, # Link Does Not Exists 13 | E032, # Cannot Delete this Link 14 | E033, # Cannot Edit this Link 15 | E034, # Error Editing Link 16 | E035, # Error Deleting Link 17 | E036 # Error Adding Link 18 | ) 19 | 20 | class Links(Resource): 21 | 22 | def get(self, public_id): 23 | """Queries the database for an existing Link item 24 | with the `public_id` 25 | 26 | :return: A Link's JSON Object representation 27 | See: app.models `Link.to_dict()` 28 | """ 29 | 30 | with current_app.app_context(): 31 | 32 | link = ( 33 | Link.query.filter_by(public_id=public_id) 34 | .first() 35 | ) 36 | 37 | if link: 38 | return JSONResponse(data=link.to_dict()) 39 | 40 | # E031 = Link Does Not Exist 41 | return JSONResponse(code=E031) 42 | 43 | @args_check(LinkValidator()) 44 | @login_required() 45 | def post(self, current_user, json_data): 46 | """Creating/Adding a new Link 47 | 48 | Note: Request must be made with as Authorized Bearer token 49 | 50 | JSON parameters: 51 | :param title: Link's title 52 | :type title: String 53 | :param description: Link's description 54 | :type description: String 55 | :param url: User's website (a valid URL) 56 | :type url: String 57 | 58 | :return: A Link's JSON Object representation 59 | See: app.models `Link.to_dict()` 60 | """ 61 | 62 | with current_app.app_context(): 63 | title = json_data.title.strip() 64 | description = json_data.description.strip() 65 | url = json_data.url.strip() 66 | 67 | link = Link( 68 | title=title, 69 | description=description, 70 | url=url, 71 | user_id=current_user.id 72 | ) 73 | 74 | link.save() 75 | 76 | # Setting Public ID 77 | link.set_public_id() 78 | 79 | return JSONResponse(data=link.to_dict()) 80 | 81 | # E036 = Error Adding Link 82 | return JSONResponse(code=E036) 83 | 84 | @args_check(LinkValidator()) 85 | @login_required() 86 | def put(self, public_id, json_data, current_user): 87 | """Editing/Updating Link 88 | 89 | Note: Request must be made with as Authorized Bearer token 90 | 91 | JSON parameters: 92 | :param title: Link's title 93 | :type title: String 94 | :param description: Link's description 95 | :type description: String 96 | :param url: User's website (a valid URL) 97 | :type url: String 98 | 99 | :return: A Link's JSON Object representation 100 | See: app.models `Link.to_dict()` 101 | """ 102 | 103 | with current_app.app_context(): 104 | title = json_data.title.strip() 105 | description = json_data.description.strip() 106 | url = json_data.url.strip() 107 | 108 | link = Link.query.filter_by(public_id=public_id).first() 109 | 110 | if not link: 111 | 112 | # E031 = Link Does Not Exist 113 | return JSONResponse(code=E031) 114 | 115 | if link.user_id != current_user.id: 116 | 117 | # E033 = User Cannot Edit Link 118 | return JSONResponse(code=E033) 119 | 120 | link.title = title 121 | link.description = description 122 | link.url = url 123 | 124 | link.save() 125 | 126 | return JSONResponse(data=link.to_dict()) 127 | 128 | # E034 = Error Editing Link 129 | return JSONResponse(code=E034) 130 | 131 | @login_required() 132 | def delete(self, current_user, public_id): 133 | """Deleting link with the `public_id` 134 | 135 | Note: Request must be made with as Authorized Bearer token 136 | 137 | :return: None 138 | """ 139 | 140 | with current_app.app_context(): 141 | link = Link.query.filter_by(public_id=public_id).first() 142 | 143 | if not link: 144 | 145 | # E031 = Link Does Not Exist 146 | return JSONResponse(code=E031) 147 | 148 | if link.user_id != current_user.id: 149 | 150 | # E032 = User Cannot Delete Link 151 | return JSONResponse(code=E032) 152 | 153 | link.deleted = True 154 | link.save() 155 | 156 | return JSONResponse(data=link.to_dict()) 157 | 158 | # E035 = Error Deleting Link 159 | return JSONResponse(code=E035) -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from app import db, bcrypt 2 | from flask import current_app 3 | 4 | import jwt 5 | from datetime import datetime, timedelta 6 | from hashlib import md5 7 | 8 | class User(db.Model): 9 | 10 | __tablename__ = 'user' 11 | 12 | id = db.Column(db.Integer, primary_key=True) 13 | name = db.Column(db.String(30), nullable=True) 14 | username = db.Column(db.String(20), nullable=False, unique=True) 15 | email = db.Column(db.String(256), nullable=False, unique=True) 16 | password = db.Column(db.String(256), nullable=False) 17 | 18 | public_id = db.Column(db.Text, nullable=True, unique=True) 19 | 20 | cover_photo = db.Column(db.Text, nullable=True) 21 | profile_photo = db.Column(db.Text, nullable=True) 22 | description = db.Column(db.Text, nullable=True) 23 | website = db.Column(db.Text, nullable=True) 24 | 25 | verified = db.Column(db.Boolean, default=False) 26 | 27 | deleted = db.Column(db.Boolean, default=False) 28 | 29 | created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) 30 | modified_at = db.Column( 31 | db.DateTime, default=db.func.current_timestamp(), 32 | onupdate=db.func.current_timestamp()) 33 | 34 | links = db.relationship('Link', order_by='Link.id', backref='user', lazy=True) 35 | social_links = db.relationship('SocialLink', order_by='SocialLink.id', backref='user', lazy=True) 36 | 37 | 38 | def __init__(self, username, email): 39 | self.username = username 40 | self.email = email 41 | 42 | def set_public_id(self): 43 | """Sets a Public ID for User""" 44 | 45 | self.public_id = get_public_id(f"{self.id}_user") 46 | self.save() 47 | 48 | def set_password(self, password): 49 | """Hashes User password""" 50 | 51 | self.password = bcrypt.generate_password_hash(password).decode() 52 | 53 | def password_is_valid(self, password): 54 | """ 55 | Checks the password against it's hash to validates the user's password 56 | """ 57 | return bcrypt.check_password_hash(self.password, password) 58 | 59 | def to_dict(self, user=None): 60 | """Returns Users data as JSON/Dictionary""" 61 | 62 | _dict = {} 63 | if not self.deleted: 64 | links = [ link.to_dict() for link in self.links if not link.deleted ] 65 | links.reverse() 66 | social_links = [social_link.to_dict() for social_link in self.social_links] 67 | _dict = { 68 | "public_id" : self.public_id, 69 | "name" : self.name, 70 | "username" : self.username, 71 | "email" : self.email if user == self else None, 72 | "description" : self.description, 73 | "website" : self.website, 74 | "verified" : self.verified, 75 | "links" : links, 76 | "social_links" : social_links 77 | } 78 | 79 | return _dict 80 | 81 | def delete(self): 82 | """Deletes User""" 83 | 84 | self.deleted = True 85 | self.save() 86 | 87 | def save(self): 88 | """Save/Updates the database""" 89 | 90 | db.session.add(self) 91 | db.session.commit() 92 | 93 | 94 | def generate_token(self, minutes=40320): 95 | """ Generates the access token""" 96 | 97 | try: 98 | # set up a payload with an expiration time 99 | payload = { 100 | 'exp': datetime.utcnow() + timedelta(minutes=minutes), 101 | 'iat': datetime.utcnow(), 102 | 'sub': self.id 103 | } 104 | # create the byte string token using the payload and the SECRET key 105 | jwt_string = jwt.encode( 106 | payload, 107 | current_app.config.get('SECRET_KEY'), 108 | algorithm='HS256' 109 | ) 110 | 111 | if type(jwt_string) == bytes: 112 | jwt_string = jwt_string.decode() 113 | 114 | return jwt_string 115 | 116 | except Exception as e: 117 | # return an error in string format if an exception occurs 118 | return str(e) 119 | 120 | @staticmethod 121 | def decode_token(token): 122 | """Decodes the access token from the Authorization header.""" 123 | 124 | try: 125 | # try to decode the token using our SECRET variable 126 | payload = jwt.decode(token, current_app.config.get('SECRET_KEY'), algorithms=['HS256']) 127 | return True, payload['sub'] 128 | except jwt.ExpiredSignatureError: 129 | # the token is expired, return an error string 130 | return False, "Expired token. Please login to get a new token" 131 | except jwt.InvalidTokenError: 132 | # the token is invalid, return an error string 133 | return False, "Invalid token. Please register or login" 134 | 135 | return False, "Invalid token. Please register or login" 136 | 137 | 138 | 139 | class Link(db.Model): 140 | __tablename__ = 'link' 141 | 142 | id = db.Column(db.Integer, primary_key=True) 143 | title = db.Column(db.String(20), nullable=False) 144 | description = db.Column(db.Text, nullable=True) 145 | url = db.Column(db.Text, nullable=True) 146 | image = db.Column(db.Text, nullable=True) 147 | public_id = db.Column(db.Text, nullable=True, unique=True) 148 | 149 | deleted = db.Column(db.Boolean, default=False) 150 | 151 | created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) 152 | modified_at = db.Column( 153 | db.DateTime, default=db.func.current_timestamp(), 154 | onupdate=db.func.current_timestamp()) 155 | 156 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) 157 | 158 | def __init__(self, title, description, url, user_id, float_value=0.0): 159 | 160 | self.title = title 161 | self.user_id = user_id 162 | self.description = description 163 | self.url = url 164 | self.float_value = float_value if float_value else 0.0 165 | 166 | def set_public_id(self): 167 | """Sets a Public ID for Link""" 168 | 169 | self.public_id = get_public_id(f"{self.id}_link") 170 | self.save() 171 | 172 | def to_dict(self): 173 | """Returns Link's data as JSON/Dictionary""" 174 | 175 | _dict = {} 176 | if not self.deleted: 177 | _dict = { 178 | "title" : self.title, 179 | "public_id" : self.public_id, 180 | "description" : self.description, 181 | "url" : self.url 182 | } 183 | 184 | return _dict 185 | 186 | def delete(self): 187 | self.deleted = True 188 | self.save() 189 | 190 | def save(self): 191 | db.session.add(self) 192 | db.session.commit() 193 | 194 | class SocialLink(db.Model): 195 | __tablename__ = 'social_link' 196 | 197 | id = db.Column(db.Integer, primary_key=True) 198 | platform_id = db.Column(db.Integer, nullable=False) 199 | url = db.Column(db.Text, nullable=False) 200 | 201 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) 202 | 203 | created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) 204 | modified_at = db.Column( 205 | db.DateTime, default=db.func.current_timestamp(), 206 | onupdate=db.func.current_timestamp()) 207 | 208 | deleted = db.Column(db.Boolean, default=False) 209 | 210 | def __init__(self, platform_id, url, user_id): 211 | self.url = url 212 | self.user_id = user_id 213 | self.platform_id = platform_id 214 | 215 | def set_public_id(self): 216 | """Sets a Public ID for SocialLink""" 217 | 218 | self.public_id = get_public_id(f"{self.id}_sociallink") 219 | self.save() 220 | 221 | def to_dict(self): 222 | """Returns SocialLink's data as JSON/Dictionary""" 223 | 224 | return { 225 | "platform_id" : self.platform_id, 226 | "url" : self.url 227 | } 228 | 229 | def save(self): 230 | db.session.add(self) 231 | db.session.commit() 232 | 233 | from app.utils.functions import get_public_id -------------------------------------------------------------------------------- /app/site.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/site.db -------------------------------------------------------------------------------- /app/users/__init__.py: -------------------------------------------------------------------------------- 1 | from app.users.routes import Users 2 | from app import api 3 | 4 | api.add_resource(Users, "/users/","/users") 5 | -------------------------------------------------------------------------------- /app/users/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/users/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/users/__pycache__/routes.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/users/__pycache__/routes.cpython-37.pyc -------------------------------------------------------------------------------- /app/users/routes.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | request, 3 | current_app 4 | ) 5 | from flask_restful import Resource 6 | from app.utils.functions import JSONResponse 7 | from app.utils.decorators import args_check, login_required 8 | from app.models import User, SocialLink 9 | from app.validators import UserValidator 10 | 11 | from app.error_codes import ( 12 | E003, # Username Already Exists" 13 | E004, # Email Already Exists" 14 | E021, # User does not exists 15 | E022, # Cannot delete this user 16 | E023, # Cannot edit this user 17 | E024, # Error editing user 18 | E025 # Error deleting user 19 | ) 20 | 21 | class Users(Resource): 22 | 23 | @login_required(optional=True) 24 | def get(self, username, current_user): 25 | 26 | """Queries the database for an existing user 27 | with the `username` 28 | 29 | :return: A user's JSON Object representation 30 | See: app.models `User.to_dict()` 31 | """ 32 | 33 | with current_app.app_context(): 34 | 35 | user = ( 36 | User.query.filter_by(username=username) 37 | .first() 38 | ) 39 | 40 | if user: 41 | return JSONResponse(data=user.to_dict(user=current_user)) 42 | 43 | # E021 = User Does Not Exist 44 | return JSONResponse(code=E021) 45 | 46 | 47 | @args_check(UserValidator()) 48 | @login_required() 49 | def put(self, current_user, json_data): 50 | """Updating/Editing user with the `username` 51 | 52 | Note: Request must be made with as Authorized Bearer token 53 | 54 | JSON parameters: 55 | :param username: User's new/existing username 56 | :type username: String 57 | :param email: User's email 58 | :type email: String 59 | :param description: User's new/existing description 60 | :type description: String 61 | :param website: User's website (a valid URL) 62 | :type website: String 63 | :param location: User's location 64 | :param type: String 65 | :param social_links: A list of dictionary objects with keys ["platform_id", "url"] 66 | :key type: Int 67 | :key url: A valid URL (String) 68 | 69 | :type social_links: List[Dict] 70 | 71 | :return: A user's JSON Object representation 72 | See: app.models `User.to_dict()` 73 | """ 74 | 75 | with current_app.app_context(): 76 | 77 | name = json_data.name.lower().strip() 78 | description = json_data.description.strip() 79 | username = json_data.username.lower().strip() 80 | email = json_data.email.lower().strip() 81 | website = request.json.get("website", "") 82 | location = request.json.get("location", "") 83 | 84 | social_links = request.json.get("social_links", None) 85 | 86 | 87 | if username != current_user.username and username: 88 | user = User.query.filter_by(username=username).first() 89 | 90 | if user: 91 | # E003 = Username Already Exists 92 | return JSONResponse(code=E003) 93 | 94 | # Update Username 95 | current_user.username = username 96 | 97 | if email != current_user.email and email: 98 | user = User.query.filter_by(email=email).first() 99 | 100 | if user: 101 | # E004 = Email Already Exists 102 | return JSONResponse(code=E004) 103 | 104 | # Update Email 105 | current_user.email = email 106 | 107 | if name: 108 | current_user.name = name 109 | 110 | if description: 111 | current_user.description = description 112 | 113 | if website: 114 | current_user.website = website 115 | 116 | if location: 117 | current_user.location = location 118 | 119 | if social_links: 120 | if type(social_links) == list: 121 | for social_link in social_links: 122 | if "platform_id" in social_link and "url" in social_link: 123 | # TODO: Check platform is if its int 124 | 125 | social_link_data = SocialLink.query.filter_by( 126 | platform_id=social_link.get("platform_id"), 127 | user_id=current_user.id 128 | ).first() 129 | 130 | if social_link_data: 131 | social_link_data.url = social_link.get("url") 132 | social_link_data.save() 133 | else: 134 | social_link_data = SocialLink ( 135 | platform_id=social_link.get("platform_id"), 136 | url=social_link.get("url"), 137 | user_id=current_user.id 138 | ) 139 | social_link_data.save() 140 | social_link_data.set_public_id() 141 | 142 | current_user.save() 143 | 144 | return JSONResponse(data=current_user.to_dict()) 145 | 146 | # E024 = Error editing user 147 | return JSONResponse(code=E024) 148 | 149 | @login_required() 150 | def delete(self, current_user): 151 | """Deleting user with the `username` 152 | 153 | Note: Request must be made with as Authorized Bearer token 154 | 155 | :return: None 156 | """ 157 | 158 | with current_app.app_context(): 159 | current_user.deleted = True 160 | current_user.save() 161 | 162 | return JSONResponse(data=current_user.to_dict()) 163 | 164 | # E025 = Error deleting user 165 | return JSONResponse(code=E025) 166 | 167 | -------------------------------------------------------------------------------- /app/utils/__pycache__/classes.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/utils/__pycache__/classes.cpython-37.pyc -------------------------------------------------------------------------------- /app/utils/__pycache__/decorators.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/utils/__pycache__/decorators.cpython-37.pyc -------------------------------------------------------------------------------- /app/utils/__pycache__/functions.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/app/utils/__pycache__/functions.cpython-37.pyc -------------------------------------------------------------------------------- /app/utils/classes.py: -------------------------------------------------------------------------------- 1 | class JSONObject(object): 2 | def __init__(self, _dict): 3 | self.keys = [] 4 | if type(_dict) == dict: 5 | self.keys = list(_dict.keys()) 6 | for k,v in _dict.items(): 7 | try: 8 | self.__setattr__(k,v) 9 | except Exception: 10 | pass 11 | 12 | def has(self,key): 13 | return key in self.keys 14 | 15 | def get(self,key): 16 | if self.has(key): 17 | return self.__getattribute__(key) 18 | return None 19 | 20 | def isnull(self,key): 21 | if self.has(key): 22 | return not bool(self.__getattribute__(key)) 23 | self.keys.append(key) 24 | self.__setattr__(key,None) 25 | return True 26 | 27 | def hasall(self,keys): 28 | for key in keys: 29 | if not self.has(key): 30 | return False 31 | return True -------------------------------------------------------------------------------- /app/utils/decorators.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from functools import wraps 3 | from app.models import User 4 | from app.utils.functions import JSONResponse 5 | from app.utils.classes import JSONObject 6 | 7 | from app.error_codes import ( 8 | E002, # Invalid Request JSON 9 | E011 # Authentication Error 10 | ) 11 | 12 | def login_required(optional=False): 13 | """ 14 | Checks and validates the Authorization Header `Bearer Token` 15 | if token is valid it'd set the `current_user` 16 | 17 | :param optional: If is set to `True` there wouldn't be any errors 18 | if the token is invalid or expired 19 | :type optional: Boolean 20 | """ 21 | 22 | def _login_required(f): 23 | @wraps(f) 24 | def decorated_function(*args, **kwargs): 25 | 26 | # E011 = Authentication Error 27 | res = JSONResponse( 28 | code=E011, 29 | message="Not authorized, You need to Login!" 30 | ) 31 | 32 | token = None 33 | 34 | try: 35 | auth_header = request.headers.get('Authorization') 36 | token = auth_header.split(" ")[1] 37 | except Exception: 38 | if not optional: 39 | return res 40 | 41 | if token: 42 | valid, user_id = User.decode_token(token.strip()) 43 | if valid: 44 | user = User.query.get(user_id) 45 | # kwargs["current_user"] = user 46 | 47 | return f(*args, **kwargs, current_user=user) 48 | 49 | if optional: 50 | return f(*args, **kwargs, current_user=None) 51 | 52 | # E011 = Authentication Error 53 | res = JSONResponse( 54 | code=E011, 55 | message="Session expired, You need to Login" 56 | ) 57 | 58 | return res 59 | 60 | return decorated_function 61 | return _login_required 62 | 63 | def args_check(validator): 64 | """ 65 | Checks and validates the JSON data been sent with a requests 66 | 67 | if JSON is valid based of the validator given it'd add 68 | the `json_data` as a `kwarg`, os it'd be avaliable for 69 | futher use. 70 | """ 71 | 72 | def decorator(f): 73 | @wraps(f) 74 | def decorated_function(*args, **kwargs): 75 | json_data = request.json if request.json else {} 76 | no_err, error_data = validator.validate(json_data) 77 | 78 | if not no_err: 79 | # E002 = Invalid Request JSON 80 | res = JSONResponse( 81 | data=error_data, 82 | code=E002, 83 | ) 84 | 85 | return res 86 | 87 | return f(*args, **kwargs, json_data=JSONObject(json_data)) 88 | return decorated_function 89 | return decorator -------------------------------------------------------------------------------- /app/utils/functions.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | from time import localtime 3 | from app.error_codes import ERRORS_DESCRIPTION, ERRORS_STATUS_CODE 4 | 5 | def JSONResponse(data=None, message=None, code=None, status=200): 6 | 7 | if (not message and code) and code in ERRORS_DESCRIPTION: 8 | message = ERRORS_DESCRIPTION.get(code,"") 9 | 10 | if code and code in ERRORS_STATUS_CODE: 11 | status = ERRORS_STATUS_CODE.get(code) 12 | 13 | if code or status not in [200, 201]: 14 | return { 15 | "code": code, 16 | "message": message, 17 | "status": status, 18 | "data":data 19 | }, status 20 | else: 21 | return data 22 | 23 | def get_public_id(unique_id): 24 | return md5(str(unique_id).encode("UTF-8")).hexdigest() 25 | 26 | -------------------------------------------------------------------------------- /app/validators.py: -------------------------------------------------------------------------------- 1 | from flask_jsonvalidator import ( 2 | JSONValidator, 3 | StringValidator, 4 | IntValidator, 5 | BooleanValidator, 6 | ArrayOfValidator 7 | ) 8 | 9 | USERNAME = "^[a-z0-9_]{3,15}$" 10 | EMAIL = "[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+" 11 | PASSWORD = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,}$" 12 | 13 | class LoginValidator(JSONValidator): 14 | validators = { 15 | "email" : StringValidator(nullable=False), 16 | "password" : StringValidator(nullable=False), 17 | } 18 | 19 | class SignupValidator(JSONValidator): 20 | validators = { 21 | "username" : StringValidator(regex=USERNAME, nullable=False), 22 | "email" : StringValidator(regex=EMAIL, nullable=False), 23 | "password" : StringValidator(nullable=False) 24 | } 25 | 26 | class LinkValidator(JSONValidator): 27 | validators = { 28 | "title" : StringValidator(nullable=False, err_msg="Title field cannot be empty"), 29 | "description" : StringValidator(nullable=True), 30 | "url" : StringValidator(nullable=False, err_msg="URL field cannot be empty") 31 | } 32 | 33 | class UserValidator(JSONValidator): 34 | validators = { 35 | "name" : StringValidator(nullable=True), 36 | "username" : StringValidator(regex=USERNAME, nullable=True, err_msg="Invalid Username"), 37 | "email" : StringValidator(regex=EMAIL, nullable=True, err_msg="Invalid Email address"), 38 | "description" : StringValidator(nullable=False, err_msg="Description field cannot be empty") 39 | } -------------------------------------------------------------------------------- /doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keosariel/Linktree-API/d00936c08e7a5015c24f29ec969d463bb738a066/doc.png -------------------------------------------------------------------------------- /manager.py: -------------------------------------------------------------------------------- 1 | from app import manager 2 | 3 | if __name__ == "__main__": 4 | manager.run() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask_SQLAlchemy==2.3.2 2 | Flask_Bcrypt==0.7.1 3 | Flask==1.0.2 4 | Flask_Migrate==2.3.0 5 | PyJWT==1.7.0 6 | gunicorn==19.9.0 7 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from app import app, db 2 | 3 | if __name__ == '__main__': 4 | db.create_all() 5 | app.run(debug=True) 6 | --------------------------------------------------------------------------------