Skip to content

Commit c1a9775

Browse files
committed
2 parents cefba13 + 98ef7aa commit c1a9775

File tree

12 files changed

+247
-54
lines changed

12 files changed

+247
-54
lines changed

server/Flask-Backend/.env.sample

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
APP_DEBUG=True
2+
APP_SECRET_KEY=supersecure
3+
ADMIN_LOGIN_TOKEN=random
4+
5+
SMS_API_KEY=
6+
SMS_LINE_NUMBER=
7+
8+
CAPTCHA_PUBLIC_KEY_V2=
9+
CAPTCHA_PRIVATE_KEY_V2=
10+
CAPTCHA_ENABLED_V2=True
11+
CAPTCHA_LOG_V2=True
12+
CAPTCHA_LANGUAGE_V2=en
13+
14+
CAPTCHA_PUBLIC_KEY_V3=
15+
CAPTCHA_PRIVATE_KEY_V3=
16+
CAPTCHA_ENABLED_V3=True
17+
CAPTCHA_SCORE_V3=0.5
18+
CAPTCHA_LOG_V3=True
19+
20+
CACHE_TYPE=NullCache
21+
22+
REDIS_DEFAULT_URI=redis://localhost:6379/0
23+
REDIS_SESSION_URI=redis://localhost:6379/1
24+
REDIS_CELERY_BACKEND_URI=redis://localhost:6379/2
25+
REDIS_CELERY_BROKER_URI=redis://localhost:6379/3
26+
REDIS_CACHE_URI=redis://localhost:6379/4
27+
REDIS_LIMIT_URI=redis://localhost:6379/5
28+
29+
DATABASE_USERNAME=
30+
DATABASE_PASSWORD=
31+
DATABASE_PORT=3306
32+
DATABASE_HOST=localhost
33+
DATABASE_NAME=mini
34+
DATABASE_TABLE_PREFIX_NAME=mini_
35+
36+
MAIL_SERVER=
37+
MAIL_PORT=587
38+
MAIL_USERNAME=
39+
MAIL_PASSWORD=
40+
MAIL_USE_TLS=True
41+
MAIL_DEBUG=False
42+
MAIL_DEFAULT_SENDER=[email protected]
43+
44+
SERVER_NAME=localhost:8000

server/Flask-Backend/app.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from core import app
2+
3+
4+
if __name__ == "__main__":
5+
app.run()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .setting import Setting

server/Flask-Backend/config/setting.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import datetime
33
from pathlib import Path
44

5+
56
import redis
67
from dotenv import load_dotenv
78
from core.utils import generate_random_string
@@ -21,6 +22,10 @@ class Setting:
2122

2223
SECRET_KEY = os.environ.get("APP_SECRET_KEY", generate_random_string())
2324

25+
API_TITLE = "UrlShorter API"
26+
API_VERSION = "v1"
27+
OPENAPI_VERSION = "3.0.2"
28+
2429
ADMIN_LOGIN_TOKEN = os.environ.get("ADMIN_LOGIN_TOKEN", "123654")
2530

2631
APP_DEBUG_STATUS = os.environ.get("APP_DEBUG", "") == "True"
@@ -47,7 +52,7 @@ class Setting:
4752
DATABASE_USERNAME = os.environ.get("DATABASE_USERNAME", "")
4853
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD", "")
4954
DATABASE_TABLE_PREFIX_NAME = os.environ.get("DATABASE_TABLE_PREFIX_NAME", "")
50-
SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{DATABASE_USERNAME}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"
55+
SQLALCHEMY_DATABASE_URI = f"sqlite:///database.sqlite3"
5156
SQLALCHEMY_TRACK_MODIFICATIONS = False
5257

5358
# Redis Config
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from flask import Flask
2+
from werkzeug.middleware.proxy_fix import ProxyFix
3+
from config import Setting
4+
5+
from .extensions import api, db, ServerMigrate
6+
from .urls import urlpatterns
7+
8+
9+
10+
def create_app(setting: Setting) -> Flask:
11+
"""
12+
Factory Function For creating FlaskApp
13+
"""
14+
app = Flask(__name__,)
15+
16+
17+
app.config.from_object(setting)
18+
19+
db.init_app(app=app) # db
20+
ServerMigrate.init_app(db=extensions.db, app=app) # migrate
21+
api.init_app(app=app)
22+
23+
for each in urlpatterns:
24+
api.register_blueprint(each["obj"], url_prefix=each["url_prefix"])
25+
26+
app.wsgi_app = ProxyFix( # tell flask in behind a reverse proxy
27+
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
28+
)
29+
return app
30+
31+
32+
app = create_app(Setting)
33+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# flask extensions
2+
3+
from flask_migrate import Migrate
4+
from flask_sqlalchemy import SQLAlchemy
5+
from flask_smorest import Api
6+
7+
8+
api = Api()
9+
db = SQLAlchemy()
10+
ServerMigrate = Migrate()
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import uuid
2+
import datetime
3+
from typing import Optional
4+
5+
import sqlalchemy as sa
6+
import sqlalchemy.orm as so
7+
from flask import current_app
8+
9+
from config import Setting
10+
from .extensions import db
11+
12+
class BaseModel(db.Model):
13+
"""
14+
Base model class for all models
15+
~~~~~~~~~~~~~~ abstract model ~~~~~~~~~~~~~~~
16+
17+
"""
18+
19+
__abstract__ = True
20+
# __table_args__ = {
21+
# # 'mysql_engine': 'InnoDB',
22+
# # 'mysql_charset': 'utf8',
23+
# # 'mysql_collate': 'utf8_persian_ci'
24+
# }
25+
26+
id: so.Mapped[int] = so.mapped_column(sa.INTEGER, primary_key=True)
27+
@staticmethod
28+
def SetTableName(name):
29+
"""Use This Method For setting a table name"""
30+
name = name.replace("-", "_").replace(" ", "")
31+
return f"{Setting.DATABASE_TABLE_PREFIX_NAME}{name}".lower()
32+
33+
def set_public_key(self):
34+
""" This Method Set a Unique PublicKey """
35+
while True:
36+
token = uuid.uuid4().hex
37+
if self.query.filter_by(public_key=token).first():
38+
continue
39+
else:
40+
self.public_key = token
41+
break
42+
43+
def save(self, show_traceback: bool = True):
44+
"""
45+
combination of two steps, add and commit session
46+
"""
47+
try:
48+
db.session.add(self)
49+
db.session.commit()
50+
except Exception as e:
51+
db.session.rollback()
52+
if show_traceback:
53+
current_app.logger.exception(exc_info=e, msg=e)
54+
return False
55+
else:
56+
return True
57+
58+
public_key: so.Mapped[str] = so.mapped_column(sa.String(36), nullable=False, unique=True)
59+
created_time: so.Mapped[Optional[datetime.datetime]] = so.mapped_column(sa.DateTime,
60+
default=datetime.datetime.now)
61+
modified_time: so.Mapped[Optional[datetime.datetime]] = so.mapped_column(sa.DateTime,
62+
onupdate=datetime.datetime.now,
63+
default=datetime.datetime.now)
64+
65+

server/Flask-Backend/core/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from shorter import blp as ShorterBluePrint
2+
3+
urlpatterns = [
4+
{"obj": ShorterBluePrint, "url_prefix": "/links/"}
5+
]

server/Flask-Backend/core/utils.py

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,13 @@
1-
import uuid
2-
import datetime
3-
import string
41
import random
5-
from urllib.parse import urlparse as url_parse
6-
from celery import Celery, Task
7-
from flask import Flask, current_app, request, session
8-
from werkzeug.utils import secure_filename as werkzeug_secure_filename
9-
10-
SysRandom = random.SystemRandom()
2+
import string
113

124

13-
def generate_random_string(length: int = 6, punctuation: bool = True) -> str:
5+
SysRandom = random.SystemRandom()
6+
def generate_random_string(length: int = 3, punctuation: bool = False) -> str:
147
"""generate random strings
15-
168
params:
179
length: int = length of random string - default is 6
1810
punctuation: bool = punctuation in random string or not
19-
2011
return:
2112
str: string: random string
2213
"""
@@ -26,44 +17,3 @@ def generate_random_string(length: int = 6, punctuation: bool = True) -> str:
2617
random_string = SysRandom.choices(letters, k=length)
2718

2819
return "".join(random_string)
29-
30-
31-
def get_next_page(fall_back_url: str = '') -> str:
32-
"""
33-
use this method for validating next params in url
34-
validate http url args next=some url
35-
"""
36-
next_page = request.args.get("next", False)
37-
if not next_page or url_parse(next_page).netloc != "":
38-
next_page = fall_back_url
39-
40-
return next_page
41-
42-
43-
44-
def make_file_name_secure(name: str, round:int=3):
45-
"""This function make sure a file name is secure
46-
remove dangerous characters and add uuid to first of file name
47-
"""
48-
name = name.replace(" ", "")
49-
name = werkzeug_secure_filename(name)
50-
return f"{''.join([uuid.uuid4().hex for _ in range(round)])}-{datetime.datetime.utcnow().date()}-{name}"
51-
52-
53-
def celery_init_app(app: Flask) -> Celery:
54-
class FlaskTask(Task):
55-
"""Every time a task is added to queue
56-
__call__ ...
57-
"""
58-
59-
def __call__(self, *args: object, **kwargs: object) -> object:
60-
with app.app_context():
61-
return self.run(*args, **kwargs)
62-
63-
celery_app = Celery(app.name, task_cls=FlaskTask)
64-
celery_app.config_from_object(app.config["CELERY"])
65-
celery_app.Task = FlaskTask
66-
celery_app.set_default()
67-
app.extensions["celery"] = celery_app
68-
return celery_app
69-
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import sqlalchemy as sa
2+
import sqlalchemy.orm as so
3+
4+
from core.extensions import db
5+
from core.models import BaseModel
6+
from core.utils import generate_random_string
7+
8+
class AliasLink(BaseModel):
9+
__tablename__ = BaseModel.SetTableName("alias-links")
10+
11+
alias_link: so.Mapped[str] = so.mapped_column(sa.String(2048), unique=True, nullable=False)
12+
link_id: so.Mapped[int] = so.mapped_column(sa.INTEGER, sa.ForeignKey(BaseModel.SetTableName("links")+".id", ondelete="CASCADE"), nullable=False, unique=False)
13+
14+
15+
class Link(BaseModel):
16+
__tablename__ = BaseModel.SetTableName("links")
17+
18+
url_address: so.Mapped[str] = so.mapped_column(sa.String(2048), unique=True, nullable=False)
19+
alias = so.relationship("AliasLink", backref="url_address", lazy="joined")
20+
21+
@staticmethod
22+
def generate_alias_url(link_id):
23+
"""generate unique alias url """
24+
default_length = 3
25+
counter = 0
26+
total_round = 0
27+
28+
while True:
29+
if total_round == 20:
30+
return None
31+
32+
if counter == 5:
33+
counter = 0
34+
total_round += 1
35+
default_length += 1
36+
37+
slug_token = generate_random_string(length=default_length)
38+
query = db.select(AliasLink).filter_by(alias_link=slug_token)
39+
result = db.session.execute(query).scalar_one_or_none()
40+
if result: # duplicated alias link
41+
counter += 1
42+
continue
43+
else:
44+
obj = AliasLink(alias_link=slug_token, link_id=link_id)
45+
obj.set_public_key()
46+
obj.save()
47+
return obj
48+
49+
50+
@classmethod
51+
def generate_alias_link(cls, url: str) -> AliasLink:
52+
query = db.select(Link).filter_by(url_address=url).distinct()
53+
result = db.session.execute(query).unique().scalar_one_or_none()
54+
if result:
55+
return result.alias
56+
57+
new_link = Link(url_address=url)
58+
new_link.set_public_key()
59+
new_link.save()
60+
alias_obj = cls.generate_alias_url(new_link.id)
61+
return alias_obj

0 commit comments

Comments
 (0)