diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 239a1ea..78ed155 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,4 @@ jobs: EXCLUDE: ".env, /test/, .gitignore, README.md" SCRIPT_AFTER: | make deploy + run: make deploy diff --git a/Makefile b/Makefile index c2f8b2f..87724f5 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,8 @@ status: build: docker compose up --build -deploy: build stop - sudo cp ./app.service /etc/systemd/system/ && \ - sudo systemctl enable app && \ - sudo systemctl restart app +deploy: + cd /var/www/cleverson.online \ + docker-compose down \ + docker-compose pull \ + docker-compose up -d \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 183fee1..ea923f0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,6 +4,8 @@ from flask_session import Session from .models import config_models +from .errorhandler import page_not_found, internal_error + db_dir = '/.database' @@ -18,27 +20,25 @@ def create_register_blueprint(app): from .auth import auth from .admin import admin from .blog import blog - + from .api import api + app.register_blueprint(auth) app.register_blueprint(admin) app.register_blueprint(blog) + app.register_blueprint(api) -def erro_handler(app): - @app.errorhandler(404) - def page_not_found(error): - return render_template('errorHandler/404.html'), 404 - - @app.errorhandler(500) - def internal_error(error): - return render_template('errorHandler/500.html'), 500 +def create_register_erro_handler(app): + app.register_error_handler(404, page_not_found) + app.register_error_handler(500, internal_error) def create_app(): - app = Flask(__name__, - template_folder='../templates', - static_folder='../static' - ) + app = Flask( + __name__, + template_folder='../templates', + static_folder='../static' + ) app.config["SQLALCHEMY_DATABASE_URI"] = f'sqlite:///{config_database_path(db_dir)}' app.config["SESSION_PERMANENT"] = False @@ -48,8 +48,7 @@ def create_app(): config_models(app) Migrate(app, app.db) create_register_blueprint(app) - - erro_handler(app) + create_register_erro_handler(app) return app diff --git a/app/admin.py b/app/admin.py index c723657..71caf6b 100644 --- a/app/admin.py +++ b/app/admin.py @@ -8,155 +8,35 @@ @admin.route('/admin', methods=['GET']) def home(): - if not session.get("token"): - return redirect('/login') + if not session.get("token"): + return redirect('/login') - draft_list = Contents.query.filter(Contents.status == "draft").all() - contents_list = Contents.query.filter(Contents.status == "published").all() + draft_list = Contents.query.filter(Contents.status == "draft").all() + contents_list = Contents.query.filter(Contents.status == "published").all() - return render_template('admin/home.html', draft_list=draft_list, contents_list=contents_list) + return render_template('admin/home.html', draft_list=draft_list, contents_list=contents_list) -@admin.route('/publish', methods=['GET', 'POST']) -def publish(): - if not session.get("token"): - return redirect('/login') - - if request.method == "POST": - title, content, description = get_title_content_description() - - if data_valid(title, content): - insert_content( - title=title, - body=content, - status='published', - accessType='public', - description=description - ) - return redirect('/') - - flash("O titulo ou texto não está preenchido adequadamente! Por favor, verifique se você preencheu os campo corretamente!") - - return render_template('admin/editor.html', is_draft_mode=True) - - -@admin.route('/update/', methods=['GET', 'POST', 'PUT']) -def update(id): - if not session.get("token"): - return redirect('/login') - if request.method == "POST": - data = request.get_json() +@admin.route('/editar/', methods=['GET']) +def editor(id): + if not session.get("token"): + return redirect('/login') + + if request.method == "GET": + post_data = Contents.query.filter(Contents.id == id).first() - title = data['title'] - body = data['body'] - description = data['description'] - - if data_valid(title, body): - update_content(id=id, title=title,body=body, description=description) - return jsonify({"status_code": 200, 'success': True}), 200 + title = post_data.title + body = post_data.body + status = post_data.status + description = post_data.description - return jsonify({"status_code":200, "success":True, "message":"O titulo ou texto não está preenchido adequadamente! Por favor, verifique se você preencheu os campo corretamente!"}), 200 - - if request.method == 'PUT': - data = request.get_json() - - title = data['title'] - body = data['body'] - description = data['description'] - - if data_valid(title, body): - update_content(id=id, title=title,body=body, status='published', description=description) - return jsonify({"status_code": 200, 'success': True}), 200 - - if request.method == "GET": - title, body, description = obtain_draft_title_and_body(id) - return render_template('admin/editor.html', id=id, title=title, body=body, description=description, is_draft_mode=False) - + return render_template('admin/editor.html', id=id, title=title, body=body, description=description, is_draft_mode=True, status=status) -@admin.route('/draft', methods=['POST']) -def draft(): - if request.method == "POST": - data = request.get_json() - title = data['title'] - content = data['body'] - description = data['description'] - - if data_valid(title, content): - insert_content( - title=title, - body=content, - status='draft', - accessType='public', - description=description - ) - return jsonify({"status_code": 200, 'success': True}), 200 - - return jsonify({ - "error": { - "status_code": 400, - "message": "Os campos título e texto não foram preenchidos adequadamente. Por favor, verifique se você preencheu os campos corretamente." - } - }), 400 - - return jsonify({"status_code": 400, "erro": "Método não permitido"}), 405 - - -@admin.route('/delete/post/', methods=['DELETE']) -def delete_post(post_id): - if not session.get("token"): - return jsonify({ - "status": "error", - "message": "Você não tem permissão para deletar este post.", - "code": 403 - }), 403 - - Contents.query.filter(Contents.id == post_id).delete() - db.session.commit() - - return jsonify({"status_code":200, "success":True}), 200 - -def obtain_draft_title_and_body(id: str): - query = Contents.query.filter(Contents.id == id) - draft_data = query.first() - title, body, description = draft_data.title, draft_data.body, draft_data.description - return title, body, description - -def get_title_content_description(): - return request.form.get("title"), request.form.get("markdown-content"), request.form.get('description') - -def data_valid(title: str, content: str) -> bool: - return len(title.strip()) > 0 and len(content.strip()) > 0 - -def update_content(id:str, title: str, body: str,description:str, status:str = None) -> None: - if status is None: - Contents.query.filter_by(id=id).update({Contents.title:title, Contents.body:body, Contents.description:description}) - db.session.commit() - return - Contents.query.filter_by(id=id).update({Contents.title:title, Contents.body:body, Contents.status:status, Contents.description:description}) - db.session.commit() - -def generate_slug(title:str) -> str: - title = unidecode(title) - slug = re.sub(r'[^\w\s-]', '', title.lower()) - slug = re.sub(r'\s', '-', slug) - return slug +@admin.route('/publicar', methods=['GET', 'POST']) +def publish(): + if not session.get("token"): + return redirect('/login') -def insert_content(title: str, body: str, status: str, accessType: str, description:str) -> None: - """ - status: "published" or "draft" - accessType: "public" or "private" - """ - db.session.add( - Contents( - id=str(uuid4()), - title=title, - body=body, - slug=generate_slug(title), - status=status, - accessType=accessType, - description=description - ) - ) - db.session.commit() + return render_template('admin/editor.html', status=None) \ No newline at end of file diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..06bb454 --- /dev/null +++ b/app/api.py @@ -0,0 +1,190 @@ +import re +from uuid import uuid4 +from unidecode import unidecode +from flask import Blueprint, request, redirect, session, render_template, flash, jsonify +from typing import Tuple, Union + +from app.models import Contents, db + + +api = Blueprint('api', __name__ ,url_prefix='/api') + + +class ContentManager: + def obtain_draft_title_and_body(self, id: str) -> Tuple[str, str,str] : + draft_data = Contents.query.filter(Contents.id == id).first() + title, body, description = draft_data.title, draft_data.body, draft_data.description + return title, body, description + + def data_valid(self, title: str, content: str) -> bool: + return len(title.strip()) > 0 and len(content.strip()) > 0 + + def update_content(self, id:str, title: str, body: str, description:str, status:str) -> None: + Contents.query.filter_by(id=id).update({Contents.title:title, Contents.body:body, Contents.status:status, Contents.description:description}) + db.session.commit() + + def generate_slug(self, title:str) -> str: + title = unidecode(title) + slug = re.sub(r'[^\w\s-]', '', title.lower()) + slug = re.sub(r'\s', '-', slug) + return slug + + def extract_json_data(self, request:request, keys:list) -> dict: + """ + Extrai dados JSON da requisição e atribui os valores às variáveis especificadas. + + Args: + request (flask.request): Objeto de requisição Flask. + keys (list): Lista de chaves para extrair do JSON. + + Returns: + dict: Dicionário com os valores extraídos das chaves especificadas. + """ + data = request.get_json() + + extracted_data = {} + for key in keys: + if key in data: + extracted_data[key] = data[key] + else: + return jsonify({ + "error": f"A chave '{key}' está ausente no JSON da requisição.", + "status_code": 400 + }), 400 + + return extracted_data + + def insert_content(self, title: str, body: str, status: str, description:str, accessType: str = 'public') -> None: + """ + status: "published" or "draft" + accessType: "public" or "private" + """ + db.session.add( + Contents( + id=str(uuid4()), + title=title, + body=body, + slug=self.generate_slug(title), + status=status, + accessType=accessType, + description=description + ) + ) + db.session.commit() + + + +@api.route('/publish',methods=['POST']) +def publish_post(): + if not session.get("token"): + return jsonify({ + "error": "acesso não autorizado.", + "status_code":401 + }), 401 + + if request.method != "POST": + return jsonify({ + "error": "Método não permitido", + "message": "Somente o método post é permitido para este terminal.", + "status_code": 405 + }), 405 + + contentManager = ContentManager() + expected_keys = ['title', 'body', 'description', 'status_publication'] + + data = contentManager.extract_json_data(request, expected_keys) + if isinstance(data, tuple): + return data + + title = data['title'] + body = data['body'] + description = data['description'] + status_publication = data['status_publication'] + + if not contentManager.data_valid(title, body): + return jsonify({ + "message": "O titulo ou texto não está preenchido adequadamente! Por favor, verifique se você preencheu os campo corretamente!", + "status_code": 400 + }), 400 + + contentManager.insert_content( + title=title, + body=body, + status=status_publication, + description=description + ) + + return jsonify({ + "message": "publicação feita com sucesso.", + "status_code": 200 + }), 200 + + +@api.route('/delete/post/', methods=['DELETE']) +def delete_post(id): + if not session.get("token"): + return jsonify({ + "error": "acesso não autorizado.", + "status_code":401 + }), 401 + + Contents.query.filter(Contents.id == id).delete() + db.session.commit() + + return jsonify({ + "message":"o post foi deletado com sucesso.", + "status_code":200 + }), 200 + + +@api.route('/update/', methods=['PUT']) +def update_post(id): + if not session.get("token"): + return jsonify({ + "error": "acesso não autorizado.", + "status_code":401 + }), 401 + + if request.method != "PUT": + + return jsonify({ + "error": "Método não permitido", + "message": "Somente o método post é permitido para este terminal.", + "status_code": 405 + }), 405 + + contentManager = ContentManager() + + expected_keys = ['title', 'body', 'description', 'status_publication'] + + data = contentManager.extract_json_data(request, expected_keys) + if isinstance(data, tuple): + return data + + title = data['title'] + body = data['body'] + description = data['description'] + status_publication = data['status_publication'] + + if not contentManager.data_valid(title, body): + return jsonify({ + "message": "O titulo ou texto não está preenchido adequadamente! Por favor, verifique se você preencheu os campo corretamente!", + "status_code": 400 + }), 400 + + contentManager.update_content( + id=id, + title=title, + body=body, + status=status_publication, + description=description + ) + + return jsonify({ + "message": f'o {title} foi atualizado com sucesso.', + "status_code": 200 + }), 200 + + + + diff --git a/app/auth.py b/app/auth.py index 4863ccf..0cc9274 100644 --- a/app/auth.py +++ b/app/auth.py @@ -2,105 +2,110 @@ from uuid import uuid4 from flask import Blueprint, redirect, session, render_template, request, flash from sqlalchemy.sql import exists +from typing import Tuple from app.models import Users, db auth = Blueprint('auth', __name__) -@auth.route('/login', methods=['GET', 'POST']) -def login(): - error = None - if request.method == 'POST': - username, password = get_username_and_password() +class Auth: + def get_hash_password(self, username:str) -> str: + hash_password = Users.query.filter_by(username=username).first().password + return hash_password - if check_username_exist(username): - hashpass = get_hash_password(username) - user_type = get_user_type(username) + def get_user_type(self,username:str) -> str: + user_type = Users.query.filter_by(username=username).first().userType + return user_type - if bcrypt.checkpw(password.encode('utf-8'), hashpass): - if user_type == 'admin': - session["token"] = uuid4() - return redirect("/admin") - else: - flash("Você não tem permissão para acessar essa página!\nÉ necessário pedir permissão para o admin da página!") - else: - flash("Usuário ou senha incorretas!") - else: - flash("Usuário não existe!") - return render_template('auth/login.html', error=error) + def check_username_exist(self, username:str) -> bool: + return Users.query.filter(Users.username == username).first() -@auth.route('/logout') -def logout(): - session["token"] = None - print('logout') - return redirect("/admin") + def get_username_and_password(self) -> Tuple[str,str]: + return request.form.get('username'), request.form.get('password') + + + def register_user(self, username:str, password:str, user_type:str) -> None: + db.session.add( + Users( + id=str(uuid4()), + username=username, + password=self.encrypt_password(password), + userType=user_type + ) + ) + db.session.commit() + + + def encrypt_password(self, password:str) -> str: + byte_password = password.encode('utf-8') + hash_password = bcrypt.hashpw(byte_password, bcrypt.gensalt()) + return hash_password -@auth.route('/register', methods=['GET', 'POST']) -def register(): - error = None - if request.method == 'POST': - username, password = get_username_and_password() - # registrar como admin - if Users.query.count() == 0: - register_user( - username=username, - password=password, - user_type='admin' - ) - return redirect("/login") - else: - username_exist = check_username_exist(username) - if not username_exist: - register_user( - username=username, - password=password, - user_type='user' - ) - return redirect("/login") - else: - error = "usuário já cadastrado!" - return render_template('auth/register.html', error=error) +@auth.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + auth = Auth() + + username, password = auth.get_username_and_password() + + if not auth.check_username_exist(username): + flash("Usuário não existe!") + return redirect("/login") + + hashpass = auth.get_hash_password(username) + user_type = auth.get_user_type(username) + if not bcrypt.checkpw(password.encode('utf-8'), hashpass): + flash("Usuário ou senha incorretas!") + return redirect("/login") + + if user_type != 'admin': + flash("Você não tem permissão para acessar essa página!\nÉ necessário pedir permissão para o administrador da página!") + return redirect("/login") -def get_hash_password(username): - hash_password = Users.query.filter_by(username=username).first().password - return hash_password + session["token"] = uuid4() + return redirect("/admin") + return render_template('auth/login.html') -def get_user_type(username): - user_type = Users.query.filter_by(username=username).first().userType - return user_type +@auth.route('/logout') +def logout(): + session["token"] = None + return redirect("/") -def check_username_exist(username): - query = Users.query.filter(Users.username == username) - result = query.first() - return result +@auth.route('/register', methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + auth = Auth() -def get_username_and_password(): - return request.form.get('username'), request.form.get('password') + username, password = auth.get_username_and_password() + # registrar como admin + if Users.query.count() == 0: + auth.register_user( + username=username, + password=password, + user_type='admin' + ) + return redirect("/login") -def register_user(username, password, user_type): - db.session.add( - Users( - id=str(uuid4()), + elif not auth.check_username_exist(username): + auth.register_user( username=username, - password=encrypt_password(password), - userType=user_type - ) - ) - db.session.commit() + password=password, + user_type='user' + ) + return redirect("/login") + + flash("Usuário já cadastrado!") + return render_template('auth/register.html') -def encrypt_password(password): - byte_password = password.encode('utf-8') - hash_password = bcrypt.hashpw(byte_password, bcrypt.gensalt()) - return hash_password diff --git a/app/errorhandler.py b/app/errorhandler.py new file mode 100644 index 0000000..2ef89d2 --- /dev/null +++ b/app/errorhandler.py @@ -0,0 +1,8 @@ +from flask import render_template + + +def page_not_found(error): + return render_template('errorHandler/404.html'), 404 + +def internal_error(error): + return render_template('errorHandler/500.html'), 500 \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index 93f3881..1b355ca 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -268,6 +268,22 @@ footer{ background: #2bb0d4; } +.bt-hide{ + border: none; + border-radius: 5px; + + padding: 10px; + + background: #afd3dd; + color: rgb(48, 48, 48); + font-size: 15px; + font-weight: bolder; +} + +.bt-hide:hover{ + cursor: pointer; + background: #a7c0c7; +} .div-buttons{ display: flex; diff --git a/static/js/buttonDeletePost.js b/static/js/buttonDeletePost.js index ba9dd61..4946a29 100644 --- a/static/js/buttonDeletePost.js +++ b/static/js/buttonDeletePost.js @@ -2,7 +2,7 @@ const BASEURL = window.location.origin const buttonDeletePost = (id) => { - fetch(BASEURL + `/delete/post/${id}`, { + fetch(BASEURL + `/api/delete/post/${id}`, { method: "DELETE", headers: { 'Accept': 'application/json', diff --git a/static/js/editorButton.js b/static/js/editorButton.js index 62f401a..8435e9c 100644 --- a/static/js/editorButton.js +++ b/static/js/editorButton.js @@ -11,10 +11,11 @@ const draftButton = () => { const dataJson = { "title": title.value, "body": body.value, - "description": description.value + "description": description.value, + "status_publication":"draft" } - fetch(BASEURL + '/draft', { + fetch(BASEURL + '/api/publish', { method: "POST", headers: { 'Accept': 'application/json', @@ -34,7 +35,7 @@ const draftButton = () => { }; -const updateButton = (draft_id) => { +const updateButton = (draft_id, publish = false) => { const title = document.getElementById("title") const body = document.getElementById("markdown-content") const description = document.getElementById("description") @@ -42,11 +43,13 @@ const updateButton = (draft_id) => { const dataJson = { "title": title.value, "body": body.value, - "description": description.value + "description": description.value, + "status_publication": publish ? "published": "draft" + } - fetch(BASEURL + `/update/${draft_id}`, { - method: "POST", + fetch(BASEURL + `/api/update/${draft_id}`, { + method: "PUT", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' @@ -64,7 +67,8 @@ const updateButton = (draft_id) => { }) } -const publish = (draft_id) => { + +const publish = () => { const title = document.getElementById("title") const body = document.getElementById("markdown-content") const description = document.getElementById("description") @@ -72,11 +76,12 @@ const publish = (draft_id) => { const dataJson = { "title": title.value, "body": body.value, - "description": description.value + "description": description.value, + "status_publication":"published" } - fetch(BASEURL + `/update/${draft_id}`, { - method: "PUT", + fetch(BASEURL + `/api/publish`, { + method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' diff --git a/templates/admin/editor.html b/templates/admin/editor.html index fa137ba..69416dc 100644 --- a/templates/admin/editor.html +++ b/templates/admin/editor.html @@ -4,7 +4,7 @@ {% include 'components/alert.html'%} -
+

Publicar novo conteúdo

@@ -23,15 +23,19 @@

Publicar novo conteúdo

- {% if is_draft_mode %} - - - {% else %} + {% if status == 'draft'%} - + + {% elif status == 'published'%} + + + {% else %} + + {% endif %}
+ {% endblock %} \ No newline at end of file diff --git a/templates/admin/home.html b/templates/admin/home.html index d5ba6d8..8c690b4 100644 --- a/templates/admin/home.html +++ b/templates/admin/home.html @@ -14,7 +14,7 @@

Admin

- + Novo conteúdo @@ -32,21 +32,26 @@

lista de rascunho

{% for content in draft_list %} - {{ content.title}} - + {{ content.title}} +
+ editar + +
{% endfor %} +

Conteudos publicados

+ {% for content in contents_list %} diff --git a/templates/components/footer.html b/templates/components/footer.html index 063a123..e564cdc 100644 --- a/templates/components/footer.html +++ b/templates/components/footer.html @@ -6,7 +6,7 @@ EmailLinkedinTabNews - 𝕏 + 𝕏-Twitter \ No newline at end of file diff --git a/templates/components/header.html b/templates/components/header.html index 51c58ab..fd89da0 100644 --- a/templates/components/header.html +++ b/templates/components/header.html @@ -1,6 +1,6 @@
diff --git a/templates/layout.html b/templates/layout.html index 86868e2..bdca387 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -4,7 +4,7 @@ - this_is_cleverson_blog + this_is_cleverson
{{ content.title}}
- editar + editar