diff --git a/Back-End/EmailTemplates/FalhaDeProjeto.html b/Back-End/EmailTemplates/FalhaDeProjeto.html new file mode 100644 index 00000000..c7288c2a --- /dev/null +++ b/Back-End/EmailTemplates/FalhaDeProjeto.html @@ -0,0 +1,22 @@ + + + + + + + +
+

Falha ao processar seu projeto 😢

+

Não foi possível concluir seu projeto na versão beta. O seguinte erro ocorreu:

+
{{erro}}
+

Nossa equipe já foi notificada. Obrigado pela compreensão.

+ +
+ + diff --git a/Back-End/EmailTemplates/SucessoDeProjeto.html b/Back-End/EmailTemplates/SucessoDeProjeto.html new file mode 100644 index 00000000..db5c7c87 --- /dev/null +++ b/Back-End/EmailTemplates/SucessoDeProjeto.html @@ -0,0 +1,48 @@ + + + + + + + +
+

✅ Projeto concluído com sucesso!

+

Olá, viemos informar que seu projeto {{title_origin}} foi executado com maestria.

+

Confira no seu painel de projetos localizado em:

+ + Acessar meu projeto + + +
+ + diff --git a/Back-End/EmailTemplates/email_account_success.html b/Back-End/EmailTemplates/email_account_success.html new file mode 100644 index 00000000..507dd81f --- /dev/null +++ b/Back-End/EmailTemplates/email_account_success.html @@ -0,0 +1,71 @@ + + + + + + + +
+

🎉 Bem-vindo(a) ao Media Cuts Studio!

+

Olá {{username}}, sua conta foi criada com sucesso.

+

Agora você já pode acessar o painel e começar a criar seus projetos de cortes automáticos!

+ + Acessar minha conta + + +
+

Junte-se à nossa comunidade e comece a escalar seu canal:

+ 💬 Grupo no WhatsApp + 🎧 Comunidade no Discord + 📱 Grupo no Telegram + 📢 Canal de notícias no Telegram +
+ + +
+ + diff --git a/Back-End/EmailTemplates/email_plan_upgraded.html b/Back-End/EmailTemplates/email_plan_upgraded.html new file mode 100644 index 00000000..b0deabc9 --- /dev/null +++ b/Back-End/EmailTemplates/email_plan_upgraded.html @@ -0,0 +1,91 @@ + + + + + + + +
+

🚀 Sua conta foi atualizada para o plano Content Creator!

+

Olá {{username}}, parabéns! Sua conta agora conta com todos os benefícios do plano:

+ +
+ Plano Content Creator — $8/mês ou $88/ano (ganhe 1 mês grátis no plano anual) +
+ + + +

Aproveite ao máximo sua assinatura! Para gerenciar seu plano ou visualizar seus projetos, acesse:

+

https://mediacutsstudio.com/shortify

+ +
+

Junte-se à nossa comunidade e comece a escalar seu canal:

+ 💬 Grupo no WhatsApp + 🎧 Comunidade no Discord + 📱 Grupo no Telegram + 📢 Canal de notícias no Telegram +
+ + +
+ + diff --git a/Back-End/EmailTemplates/email_tiktok_fail.html b/Back-End/EmailTemplates/email_tiktok_fail.html new file mode 100644 index 00000000..5237cdff --- /dev/null +++ b/Back-End/EmailTemplates/email_tiktok_fail.html @@ -0,0 +1,47 @@ + + + + + + + +
+

❌ Falha ao enviar vídeo para o TikTok

+

Olá, não foi possível enviar o vídeo vertical {{title}} para o TikTok.

+

O seguinte erro aconteceu:

+
{{errupload1}}
+

Nossa equipe já foi notificada e investigará o problema.

+ +
+ + diff --git a/Back-End/EmailTemplates/email_youtube_fail.html b/Back-End/EmailTemplates/email_youtube_fail.html new file mode 100644 index 00000000..67d82a02 --- /dev/null +++ b/Back-End/EmailTemplates/email_youtube_fail.html @@ -0,0 +1,47 @@ + + + + + + + +
+

❌ Falha ao enviar Shorts para o YouTube

+

Olá, não foi possível enviar o Shorts {{title}} para o YouTube.

+

O seguinte erro aconteceu:

+
{{error_content}}
+

Nossa equipe já foi notificada e investigará o problema.

+ +
+ + diff --git a/Back-End/EmailTemplates/server_limit.html b/Back-End/EmailTemplates/server_limit.html new file mode 100644 index 00000000..d101b19f --- /dev/null +++ b/Back-End/EmailTemplates/server_limit.html @@ -0,0 +1,46 @@ + + + + + + + +
+

⚠️ Limitação do servidor

+

Olá, não foi possível colocar seu projeto + {{title}} em execução por conta da limitação do servidor.

+

Mas não se preocupe! Agendamos a execução do seu projeto para o seguinte horário:

+

{{new_scheduled_time}}

+

Você pode acompanhar no painel de projetos:

+ + https://mediacutsstudio.com/projects + + +
+ + diff --git a/Back-End/EmailTemplates/teste.html b/Back-End/EmailTemplates/teste.html new file mode 100644 index 00000000..4f9614ac --- /dev/null +++ b/Back-End/EmailTemplates/teste.html @@ -0,0 +1,240 @@ + + + + + + + + + New Message + + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + + + + +
Logo
+ + + + + +
+ + + + +
+ + + + + + + + + + +
+ + + + +
+ + + +
+ + + + +
+ + + + + + + + + + + + + +
Giving tuesday

Help Feed the Needy

This day encourages people to come forward and volunteer towards social work or charity work and help the less fortunate people. Let us all spread the vital message of this momentous day and let us be grateful for what we have.

Help the Needy
+ + + + +
+ + + + +
+ + + + + + + + + +
+ + + + +
+ + + + +
+ + + + +
+
+ + \ No newline at end of file diff --git a/Back-End/Modules/Geters/user_by_email.py b/Back-End/Modules/Geters/user_by_email.py new file mode 100644 index 00000000..f97f27a5 --- /dev/null +++ b/Back-End/Modules/Geters/user_by_email.py @@ -0,0 +1,8 @@ + +from Models.postgreSQL import db, User +from datetime import datetime, timedelta + +def get_user_by_email(email): + if not email: + return None + return User.query.filter_by(email=email).first() diff --git a/Back-End/Modules/Resolvers/generate_invoice_pdf.py b/Back-End/Modules/Resolvers/generate_invoice_pdf.py new file mode 100644 index 00000000..752ffe37 --- /dev/null +++ b/Back-End/Modules/Resolvers/generate_invoice_pdf.py @@ -0,0 +1,38 @@ +from reportlab.lib.pagesizes import A4 +from reportlab.pdfgen import canvas +import os +import json + +def generate_invoice_pdf(invoice, output_dir="../../Invoices"): + pdf_filename = f"invoice_{invoice.id}.pdf" + pdf_path = os.path.join(output_dir, pdf_filename) + + c = canvas.Canvas(pdf_path, pagesize=A4) + width, height = A4 + + # Cabeçalho + c.setFont("Helvetica-Bold", 16) + c.drawString(50, height - 50, "INVOICE") + + # Informações do usuário + c.setFont("Helvetica", 12) + c.drawString(50, height - 100, f"Invoice Number: {invoice.number}") + c.drawString(50, height - 120, f"Date: {invoice.date.strftime('%Y-%m-%d')}") + c.drawString(50, height - 140, f"User ID: {invoice.user_id}") + c.drawString(50, height - 160, f"Plan: {invoice.plan_name}") + + # Itens + c.drawString(50, height - 200, "Items:") + y = height - 220 + lines = json.loads(invoice.lines) if invoice.lines else [] + for line in lines: + c.drawString(60, y, f"{line['description']} x {line['qty']} - ${line['price']:.2f}") + y -= 20 + + # Total + c.drawString(50, y - 20, f"Total Amount: ${float(invoice.amount):.2f} {invoice.currency}") + + c.showPage() + c.save() + print(f"pdf_path {pdf_path}") + return pdf_path diff --git a/Back-End/Modules/Resolvers/send_email.py b/Back-End/Modules/Resolvers/send_email.py new file mode 100644 index 00000000..e5edd062 --- /dev/null +++ b/Back-End/Modules/Resolvers/send_email.py @@ -0,0 +1,171 @@ + + + + + +import os +from dotenv import load_dotenv +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import logging +import os + +diretorio_script = os.path.dirname(os.path.abspath(__file__)) +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') +os.makedirs(os.path.join(diretorio_script, "../", '../', 'Logs'), exist_ok=True) +file_handler = logging.FileHandler(os.path.join(diretorio_script, '../',"../", 'Logs', 'send_email.log')) +file_handler.setFormatter(formatter) +console_handler = logging.StreamHandler() +console_handler.setFormatter(formatter) +logger.addHandler(file_handler) +logger.addHandler(console_handler) + +load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), + "../", + "../", + "Keys", + "keys.env" + )) + +def SendEmail( + user_email_origin="", + html_attach_flag=True, + email_type="Failed Project", + + SMTP_ADM="", + SMTP_PASSWORD="", + SMTP_HOST="", + SMTP_PORT="", + use_tls="", + + erro_project="", + title_origin="", + new_scheduled_time="", + planname='' + ): + """ + email_type: "Sucess Upgrated Account", "Failed Project", "Sucess Project", "Server Limitation", "Tiktok Publish Fail", "Youtube Publish Fail", "Sucess Created Account" + """ + template_path = os.path.join(diretorio_script, '../', '../', "EmailTemplates") + with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as SMTP_server: + SMTP_server.connect(SMTP_HOST, SMTP_PORT) + if use_tls: + SMTP_server.starttls() + SMTP_server.login(SMTP_ADM, SMTP_PASSWORD) + MIME_server = MIMEMultipart() + MIME_server['From'] = f"Media Cuts Studio <{SMTP_ADM}>" + MIME_server['To'] = user_email_origin + try: + if email_type == "Failed Project": + corpo = f""" + Olá Nao foi possivel executar seu projeto com maestria pedimos desculpas e a compreencao que é natural que ocorra erros na versao beta + enviaremos o seguinte erro para os desenvolvedores: \n {erro_project} \n + """ + MIME_server['Subject'] = "O projeto falhou" + html_body = open(os.path.join(template_path, "FalhaDeProjeto.html"), encoding='utf-8').read().replace("{{erro}}", str(erro_project)) + + elif email_type == "Sucess Project": + corpo = f""" + Olá viemos informar que seu projeto\n + {title_origin}\n + foi executado com maestria confira no seu painel de projetos localizado em\n + https://mediacutsstudio.com/projects + """ + MIME_server['Subject'] = "O projeto foi um sucesso" + html_body = open(os.path.join(template_path, "SucessoDeProjeto.html"), encoding='utf-8').read().replace("{{title_origin}}", str(title_origin)) + + elif email_type == "Server Limitation": + corpo = f""" + Olá Nao foi possivel colocar seu projeto {title_origin} em execucao por conta da limitacao do servidor\n + Mas nao se preocupe agendamos a execucao do seu projeto para o seguinte horario: \n + {new_scheduled_time} + """ + MIME_server['Subject'] = "Limitacao do servidor" + html_body = open(os.path.join(template_path, "server_limit.html"), encoding='utf-8').read().replace("{{title}}", title_origin).replace("{{new_scheduled_time}}", new_scheduled_time) + + elif email_type == "Tiktok Publish Fail": + corpo = f""" + Olá Nao foi possivel enviar o video vertical {title_origin} para o tiktok\n + o seguinte erro aconteceu: \n {erro_project} \n + """ + MIME_server['Subject'] = "Nao foi possivel enviar o Tiktok" + html_body = open(os.path.join(template_path, "email_tiktok_fail.html"), encoding='utf-8').read().replace("{{title}}", title_origin).replace("{{errupload1}}", str(erro_project)) + + elif email_type == "Youtube Publish Fail": + corpo = f""" + Olá Nao foi possivel enviar o shorts {title_origin} para o youtube\n + o seguinte erro aconteceu: \n + {erro_project} + """ + MIME_server['Subject'] = "Nao foi possivel enviar o Youtube" + html_body = open(os.path.join(template_path, "email_youtube_fail.html"), encoding='utf-8').read().replace("{{title}}", title_origin).replace("{{error_content}}", str(erro_project)) + + elif email_type == "Sucess Created Account": + corpo = f""" + 🎉 Bem-vindo(a) ao Media Cuts Studio! + Olá {user_email_origin}, sua conta foi criada com sucesso. + Agora você já pode acessar o painel e começar a criar seus projetos de cortes automáticos! + https://mediacutsstudio.com/login + Media Cuts Studio © 2025 + + """ + MIME_server['Subject'] = "Conta criada com sucesso - Media Cuts Studio" + html_body = open(os.path.join(template_path, "email_account_success.html"), encoding='utf-8').read().replace("{{username}}", user_email_origin) + + elif email_type == "Sucess Upgrated Account": + corpo = f""" + 🎉 Bem-vindo(a) ao Media Cuts Studio! + Olá {user_email_origin}, sua conta foi atualizada com sucesso. + Agora você já pode acessar o painel e começar a criar seus projetos de cortes automáticos! + https://mediacutsstudio.com/login + Media Cuts Studio © 2025 + + """ + MIME_server['Subject'] = f"Seu conta foi atualizada para o Plano {planname} 🚀" + html_body = open(os.path.join(template_path, "email_plan_upgraded.html"), encoding='utf-8').read().replace("{{username}}", user_email_origin) + + if html_attach_flag == True: + MIME_server.attach(MIMEText(html_body, "html")) + elif html_attach_flag == False: + MIME_server.attach(MIMEText(corpo, "plain")) + + SMTP_server.sendmail(SMTP_ADM, user_email_origin, MIME_server.as_string()) + logger.info(f"Email '{email_type}' enviado com sucesso!") + SMTP_server.quit() + except Exception as eerrorsendemail: + logger.warning(f"erro ao enviar o email de sinalizacao de erro {eerrorsendemail}") + + + +# if __name__ == '__main__': + +# host = os.getenv('SMTP_HOST') +# port = int(os.getenv('SMTP_PORT', 587)) +# SMTP_USER = os.getenv('SMTP_USER') +# password = os.getenv('SMTP_PASSWORD') +# use_tls = os.getenv('SMTP_USE_TLS', 'true').lower() == 'true' +# user_email_origin = "freitasalexandre810@gmail.com" +# erro_project = "erro de teste" +# title_origin = "projeto de teste" +# new_scheduled_time = "2025-08-05 21:42:04" + + +# SendEmail( +# user_email_origin=user_email_origin, +# html_attach_flag=True, +# email_type="Sucess Created Account", + +# SMTP_ADM=SMTP_USER, +# SMTP_PASSWORD=password, +# SMTP_HOST=host, +# SMTP_PORT=port, +# use_tls=use_tls, + + +# erro_project=erro_project, +# title_origin=title_origin, +# new_scheduled_time=new_scheduled_time +# ) diff --git a/Back-End/Modules/Resolvers/user_identifier.py b/Back-End/Modules/Resolvers/user_identifier.py index 6b265f24..6d19d90f 100644 --- a/Back-End/Modules/Resolvers/user_identifier.py +++ b/Back-End/Modules/Resolvers/user_identifier.py @@ -72,27 +72,43 @@ def resolve_user_identifier(identifier): # fallback: tenta buscar por email ignorando espaços return User.query.filter_by(email=str(identifier).strip()).first() -def auth_user(logs_collection, app): +def auth_user(logs_collection, app, email='', password=''): with app.app_context(): - header_token = None - auth_header = request.headers.get('Authorization') - if auth_header and auth_header.lower().startswith('bearer '): - header_token = auth_header.split(None, 1)[1].strip() - if not header_token: - header_token = request.headers.get('X-API-TOKEN') + header_token = request.headers.get('X-API-TOKEN') user = None - # se token informado, resolve usuário if header_token: try: user = get_user_by_access_token(header_token) + if user: + logger.info(f"auth_user login sucess") + return user, user.id, "success" + else: + logger.info(f"auth_user invalid token") + + user = resolve_user_identifier(email) + # evita usar user.id quando user é None (corrige crash no log) + if not user or not user.check_password(password): + log_action(logs_collection, 'login_failed', {'message': 'login_failed in if not user or not user.check_password(password):'}, level='warning', user=(user.id if user else None)) + return None, None, "invalid" + else: + logger.info(f"auth_user login sucess") + return user, user.id, "success" + except Exception as e: - log_action(logs_collection, 'dashboard_token_lookup_error', {'error': str(e)}, level='warning') - return None, None, "invalid" - + logger.info(f"auth_user_error {e}") + log_action( + logs_collection, + 'auth_user_error', + {'message': str(e)}, + level='warning' + ) + return None, None, "invalid" + + if not user: + return None, None, "invalid" - numeric_user_id = user.id - return user, numeric_user_id, "success" + return user, user.id, "success" def auth_user_fallback(email, password, logs_collection, app): """ diff --git a/Back-End/TestDiscovery/create_new_user.py b/Back-End/TestDiscovery/create_new_user.py new file mode 100644 index 00000000..b3e3d982 --- /dev/null +++ b/Back-End/TestDiscovery/create_new_user.py @@ -0,0 +1,29 @@ +import requests + +API_URL = "https://api.softwareai.site/api/register" + +def criar_conta(email: str, senha: str, expires_days: int = None): + payload = { + "email": email, + "password": senha, + } + if expires_days is not None: + payload["expires_days"] = expires_days + + try: + response = requests.post(API_URL, json=payload, timeout=10) + response.raise_for_status() + data = response.json() + print("✅ Conta criada com sucesso!") + print(f"User ID: {data.get('user_id')}") + print(f"Token: {data.get('acess_token')}") + print(f"Expira em: {data.get('expires_at')}") + return data + except requests.exceptions.HTTPError as http_err: + print(f"❌ Erro HTTP: {http_err} -> {response.text}") + except requests.exceptions.RequestException as err: + print(f"❌ Erro de requisição: {err}") + +# Exemplo de uso +if __name__ == "__main__": + criar_conta("teste@example.com", "minhasenha123", expires_days=30) diff --git a/Back-End/api.py b/Back-End/api.py index 12c16929..e55432ea 100644 --- a/Back-End/api.py +++ b/Back-End/api.py @@ -5,8 +5,8 @@ import json import logging from dotenv import load_dotenv - - +import stripe +from decimal import Decimal from bson.json_util import dumps from datetime import datetime, timedelta, timezone from flask import g, Flask, Response, request, jsonify, send_file @@ -18,6 +18,7 @@ from Models.postgreSQL import * from Modules.Resolvers.pr_process import process_pull_request +from Modules.Resolvers.generate_invoice_pdf import generate_invoice_pdf from Modules.Resolvers.user_identifier import auth_user, require_user_token, resolve_user_identifier from Modules.Geters.systemsettings import * from Modules.Geters.user_by_access_token import get_user_by_access_token @@ -34,16 +35,39 @@ from Modules.Savers.log_system_health import log_system_health from Modules.Savers.log_action import log_action - +from Modules.Resolvers.send_email import SendEmail +from Modules.Geters.user_by_email import get_user_by_email + + +diretorio_script = os.path.dirname(os.path.abspath(__file__)) +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') +os.makedirs(os.path.join(diretorio_script, 'Logs'), exist_ok=True) +file_handler = logging.FileHandler(os.path.join(diretorio_script, 'Logs', 'api.log')) +file_handler.setFormatter(formatter) +console_handler = logging.StreamHandler() +console_handler.setFormatter(formatter) +logger.addHandler(file_handler) +logger.addHandler(console_handler) os.chdir(os.path.join(os.path.dirname(__file__))) load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), 'keys.env')) -INVOICES_DIR = os.path.join(os.path.dirname(__file__), 'invoices') +INVOICES_DIR = os.path.join(os.path.dirname(__file__), 'Invoices') os.makedirs(INVOICES_DIR, exist_ok=True) - - +ADMIN_API_KEY = "apikey-Api-Landingpage-ZBQ2x5m_ae8Ubke9cI664PeCkerEp6EMHDyeriFFjq8" +host = os.getenv('SMTP_HOST') +port = int(os.getenv('SMTP_PORT', 587)) +SMTP_USER = os.getenv('SMTP_USER') +SMTP_PASSWORD = os.getenv('SMTP_PASSWORD') +use_tls = os.getenv('SMTP_USE_TLS', 'true').lower() == 'true' +createcheckout = os.getenv("createcheckout") +stripe.api_key = os.getenv("STRIPE_SECRET_KEY") +WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET") +success_url = os.getenv("success_url") +cancel_url = os.getenv("cancel_url") app = Flask(__name__) asgi_app = WsgiToAsgi(app) @@ -67,11 +91,8 @@ if os.getenv("FLASK_ENV") == "development": - CORS(app, resources={ - r"/api/*": { - "origins": os.getenv('FRONTEND_ORIGINS', '*').split(',') - } - }) + CORS(app, origins=os.getenv("FRONTEND_ORIGINS", "*").split(",")) + db.init_app(app) @app.route('/') @@ -184,7 +205,9 @@ def register(): # @require_user_token(optional=True) def login(): try: - user, access_token_to_return, status = auth_user(logs_collection, app) + email = request.args.get('email') + password = request.args.get('password') + user, access_token_to_return, status = auth_user(logs_collection, app, email, password) if status == "invalid" or not user: return jsonify({"error": "Credenciais inválidas"}), 401 @@ -1324,6 +1347,220 @@ def prai(): }), 202 + + + + + +@app.route("/api/billing/checkout", methods=["POST"]) +def create_checkout(): + data = request.get_json() + try: + + plan = data["plan"] + billingCycle = data["billingCycle"] + if billingCycle == "monthly": + if plan == "Premium": + SUBSCRIPTION_PRICE_ID = os.getenv("STRIPE_SUBSCRIPTION_PRICE_ID_Premium") + elif plan == "Pro": + SUBSCRIPTION_PRICE_ID = os.getenv("STRIPE_SUBSCRIPTION_PRICE_ID_Pro") + + elif billingCycle == "annual": + if plan == "Premium": + SUBSCRIPTION_PRICE_ID = os.getenv("STRIPE_SUBSCRIPTION_PRICE_ID_Premium_anual") + elif plan == "Pro": + SUBSCRIPTION_PRICE_ID = os.getenv("STRIPE_SUBSCRIPTION_PRICE_ID_Pro_anual") + + session = stripe.checkout.Session.create( + line_items=[{ + "price": SUBSCRIPTION_PRICE_ID, + "quantity": 1 + }], + mode="subscription", + payment_method_types=["card"], + success_url=success_url, + cancel_url=cancel_url, + metadata={"email": data["email"], + "password": data["password"], + "SUBSCRIPTION_PLAN": data["plan"], + }, + ) + print("Sessão criada:", session.id) + return jsonify({"sessionId": session.id}) + except Exception as e: + print("Erro ao criar a sessão de checkout:", e) + return jsonify({"error": str(e)}), 500 + +@app.route('/proxy-checkout', methods=['POST']) +def proxy_checkout(): + try: + data = request.get_json() + headers = { + "Content-Type": "application/json", + "Api-Landingpage-API-KEY": ADMIN_API_KEY + } + response = requests.post(createcheckout, json=data, headers=headers) + return jsonify(response.json()), response.status_code + except Exception as e: + logger.info(f"Erro no servidor {e}") + return jsonify({"error": f"Erro no servidor {e}"}), 500 + +# ------------------------------------------------------------------- +# Endpoint Webhook Stripe +@app.route("/webhook", methods=["POST"]) +def stripe_webhook(): + """ + Endpoint para tratar os webhooks enviados pela Stripe. + """ + payload = request.data + sig_header = request.headers.get("Stripe-Signature") + event = None + + try: + event = stripe.Webhook.construct_event(payload, sig_header, WEBHOOK_SECRET) + except ValueError as e: + return jsonify({"message": "Invalid payload"}), 400 + except stripe.error.SignatureVerificationError as e: + return jsonify({"message": "Invalid signature"}), 400 + + # Processa o evento conforme o seu tipo + if event["type"] == "checkout.session.completed": + session = event["data"]["object"] + + email_metadata = session["metadata"].get("email") + password_metadata = session["metadata"].get("password") + SUBSCRIPTION_PLAN = session["metadata"].get("SUBSCRIPTION_PLAN") + + user = get_user_by_email(email_metadata) + if not user: + new_user = User(email=email_metadata) + new_user.set_password(password_metadata) + acess_token = new_user.create_access_token_for_user(TOKEN_DEFAULT_EXPIRES_DAYS) + db.session.add(new_user) + db.session.commit() + log_action(logs_collection, 'user_registered', {'message': "Usuário criado com sucesso"}) + numeric_user_id = user.id + else: + numeric_user_id = user.id + + if session.get("payment_status") == "paid": + + log_action(logs_collection, + 'payment_system', + {'message': f"Pagamento por cartão com sucesso: {SUBSCRIPTION_PLAN} {email_metadata}"}, + user=numeric_user_id + ) + + plans = get_plans_data() + payload = plans[SUBSCRIPTION_PLAN] + user.limit_monthly_tokens = payload.get('limit_monthly_tokens') + user.tokens_used = 0 + user.plan_name = SUBSCRIPTION_PLAN + user.expires_at = datetime.utcnow() + timedelta(days=30) + user.revoked_at = None + invoice = Invoice( + user_id=numeric_user_id, + number=f"INV-{int(datetime.utcnow().timestamp())}", # um número único simples + date=datetime.utcnow(), + amount=Decimal(payload.get('price', 0.0)), + currency='USD', + status='paid', + plan_name=SUBSCRIPTION_PLAN, + lines=json.dumps([{"description": "PR-AI Subscription paid", "qty": 1, "price": payload.get('price', 0.0)}]) + ) + db.session.add(invoice) + db.session.commit() + + pdf_path = generate_invoice_pdf(invoice, output_dir=INVOICES_DIR) + invoice.pdf_path = pdf_path + db.session.commit() + + SendEmail( + user_email_origin=email_metadata, + html_attach_flag=True, + email_type="Sucess Upgrated Account", + SMTP_ADM=SMTP_USER, + SMTP_PASSWORD=SMTP_PASSWORD, + SMTP_HOST=host, + SMTP_PORT=port, + use_tls=use_tls, + erro_project="", + title_origin="", + new_scheduled_time="", + planname=SUBSCRIPTION_PLAN + ) + + + elif session.get("payment_status") == "unpaid" and session.get("payment_intent"): + payment_intent = stripe.PaymentIntent.retrieve(session["payment_intent"]) + hosted_voucher_url = ( + payment_intent.next_action + and payment_intent.next_action.get("boleto_display_details", {}) + .get("hosted_voucher_url") + ) + if hosted_voucher_url: + user_email = session.get("customer_details", {}).get("email") + print("Gerou o boleto e o link é", hosted_voucher_url) + log_action(logs_collection, + 'payment_system', + {'message': f"Gerou o boleto e o link é {hosted_voucher_url}"}, + user=numeric_user_id + ) + + + + elif event["type"] == "checkout.session.expired": + session = event["data"]["object"] + if session.get("payment_status") == "unpaid": + teste_id = session["metadata"].get("testeId") + print("Checkout expirado", teste_id) + log_action(logs_collection, + 'payment_system', + {'message': f"Checkout expirado {teste_id}"}, + ) + + elif event["type"] == "checkout.session.async_payment_succeeded": + session = event["data"]["object"] + if session.get("payment_status") == "paid": + teste_id = session["metadata"].get("testeId") + print("Pagamento boleto confirmado", teste_id) + log_action(logs_collection, + 'payment_system', + {'message': f"Pagamento boleto confirmado {teste_id}"}, + ) + + elif event["type"] == "checkout.session.async_payment_failed": + session = event["data"]["object"] + if session.get("payment_status") == "unpaid": + teste_id = session["metadata"].get("testeId") + print("Pagamento boleto falhou", teste_id) + log_action(logs_collection, + 'payment_system', + {'message': f"Pagamento boleto falhou {teste_id}"}, + ) + + elif event["type"] == "customer.subscription.deleted": + print("Cliente cancelou o plano") + log_action(logs_collection, + 'payment_system', + {'message': f"Cliente cancelou o plano"}, + ) + + return jsonify({"result": event, "ok": True}) + + + + + + + + + + + + + + # Endpoints nao desenvolvidos e Pendentes def deploy_containers_internal(triggered_by=None, source='manual'): diff --git a/Back-End/requirements.txt b/Back-End/requirements.txt index 9b88a110..c7f3972e 100644 --- a/Back-End/requirements.txt +++ b/Back-End/requirements.txt @@ -5,7 +5,6 @@ Flask-JWT-Extended==4.6.0 Flask-SQLAlchemy==3.1.1 sqlalchemy==2.0.25 -python-dotenv openai-agents asyncio uvicorn @@ -17,7 +16,18 @@ python-dotenv==1.0.0 requests==2.31.0 bcrypt==4.1.2 - +reportlab tiktoken +celery +redis +google-api-python-client +google-auth-oauthlib +google-auth-httplib2 +pytz +waitress +gunicorn +stripe + + diff --git a/Front-End/.env b/Front-End/.env index 2b1bb692..466e9f8d 100644 --- a/Front-End/.env +++ b/Front-End/.env @@ -1 +1,2 @@ -VITE_BACK_END=http://localhost:5920 \ No newline at end of file +VITE_BACK_END=http://localhost:5910 +VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51QpX90Cvm2cRLHtdoF7n2Ea4sRRjYBx8Csiii0e6M6ECTJJ8fKaQ1DKpJApfJZH5hIkWRojaMmaxY9sEcS50tspB00DF2IA12h \ No newline at end of file diff --git a/Front-End/Dockerfile b/Front-End/Dockerfile index 5dfe1e14..83058a50 100644 --- a/Front-End/Dockerfile +++ b/Front-End/Dockerfile @@ -17,6 +17,10 @@ RUN npm install --legacy-peer-deps RUN npm install vite --save-dev # RUN npm install @rollup/rollup-linux-x64-gnu RUN npm i --save-dev @types/node +RUN npm install @stripe/stripe-js + +RUN npm install framer-motion axios react-icons + # 6. Coloca o binário local no PATH (para que 'vite' seja encontrado) ENV PATH /app/node_modules/.bin:$PATH # Copia todo o código da landingpage diff --git a/Front-End/package-lock.json b/Front-End/package-lock.json index 520178de..ae000901 100644 --- a/Front-End/package-lock.json +++ b/Front-End/package-lock.json @@ -36,12 +36,15 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", + "@stripe/stripe-js": "^7.9.0", "@tanstack/react-query": "^5.56.2", + "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", "date-fns": "^3.6.0", "embla-carousel-react": "^8.3.0", + "framer-motion": "^12.23.22", "input-otp": "^1.2.4", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", @@ -49,6 +52,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", + "react-icons": "^5.5.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", "recharts": "^2.12.7", @@ -2542,6 +2546,15 @@ "win32" ] }, + "node_modules/@stripe/stripe-js": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz", + "integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@swc/core": { "version": "1.7.39", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.39.tgz", @@ -3290,6 +3303,12 @@ "node": ">=10" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -3328,6 +3347,17 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3402,6 +3432,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3911,6 +3954,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4120,6 +4175,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -4148,6 +4212,20 @@ "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4195,6 +4273,51 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -4581,6 +4704,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -4597,6 +4740,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -4611,6 +4770,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.22", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz", + "integrity": "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.21", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4634,6 +4820,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -4643,6 +4853,19 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4712,6 +4935,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -4729,6 +4964,33 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -5511,6 +5773,15 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5533,6 +5804,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5555,6 +5847,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion-dom": { + "version": "12.23.21", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.21.tgz", + "integrity": "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5968,6 +6275,12 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6053,6 +6366,15 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", diff --git a/Front-End/package.json b/Front-End/package.json index a252135d..146e3646 100644 --- a/Front-End/package.json +++ b/Front-End/package.json @@ -39,12 +39,15 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", + "@stripe/stripe-js": "^7.9.0", "@tanstack/react-query": "^5.56.2", + "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", "date-fns": "^3.6.0", "embla-carousel-react": "^8.3.0", + "framer-motion": "^12.23.22", "input-otp": "^1.2.4", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", @@ -52,6 +55,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", + "react-icons": "^5.5.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", "recharts": "^2.12.7", diff --git a/Front-End/src/App.tsx b/Front-End/src/App.tsx index 8de1364c..c808f009 100644 --- a/Front-End/src/App.tsx +++ b/Front-End/src/App.tsx @@ -18,6 +18,15 @@ import BillingPage from "./pages/Billing"; import InvoicesPage from "./pages/Invoices"; import Index from "./pages/Landingpage"; +import SignupCheckout from "./pages/SignupCheckout"; +import CheckoutSuccess from "./pages/CheckoutSuccess"; +import CheckoutError from "./pages/CheckoutError"; + + + + + + import { AuthProvider, useAuth } from "./contexts/AuthContext"; import ProtectedRoute from "./components/ProtectedRoute"; @@ -48,8 +57,12 @@ const AppRoutes = () => { } /> - } /> + + } /> + } /> + } /> + } /> )} diff --git a/Front-End/src/components/app-sidebar.tsx b/Front-End/src/components/app-sidebar.tsx index 339f6edc..b11cf42a 100644 --- a/Front-End/src/components/app-sidebar.tsx +++ b/Front-End/src/components/app-sidebar.tsx @@ -122,7 +122,7 @@ export function AppSidebar() { {!isCollapsed && (

PR AI

-

Sistema de DevOps

+

Equipe de DevOps

)} diff --git a/Front-End/src/components/pricing-section.tsx b/Front-End/src/components/pricing-section.tsx index 9bc1a397..681ec39a 100644 --- a/Front-End/src/components/pricing-section.tsx +++ b/Front-End/src/components/pricing-section.tsx @@ -1,3 +1,4 @@ +// Front-End\src\components\pricing-section.tsx import { Check, Star, Zap } from "lucide-react"; import { useEffect, useState } from "react"; import { project } from "@/constants/landingpage.ts"; @@ -120,7 +121,19 @@ export const PricingSection = () => { {/* CTA Button */} - {plan.isPopular && (
diff --git a/Front-End/src/pages/CheckoutError.tsx b/Front-End/src/pages/CheckoutError.tsx new file mode 100644 index 00000000..d0f79ad2 --- /dev/null +++ b/Front-End/src/pages/CheckoutError.tsx @@ -0,0 +1,20 @@ +// src/components/CheckoutError.tsx +import React from "react"; +import { Link } from "react-router-dom"; + +export default function CheckoutError(): JSX.Element { + return ( +
+

Ocorreu um erro no pagamento

+

+ Não foi possível processar sua assinatura. Por favor, tente novamente ou contate nosso suporte. +

+ + Voltar ao Checkout + +
+ ); +} diff --git a/Front-End/src/pages/CheckoutSuccess.tsx b/Front-End/src/pages/CheckoutSuccess.tsx new file mode 100644 index 00000000..4a584fb1 --- /dev/null +++ b/Front-End/src/pages/CheckoutSuccess.tsx @@ -0,0 +1,38 @@ +// src/components/CheckoutSuccess.tsx +import React from "react"; +import { Link } from "react-router-dom"; + +export default function CheckoutSuccess(): JSX.Element { + const contentCreatorFeatures = [ + "Tudo do plano Startup +", + "60 vídeos base por mês (Totalizando 90 por mês)", + "Cortes em 2K (Quad HD)", + "+ 1 projeto em simultâneo (Totalizando 2 em simultâneo)", + "Remoção de marca d´água nos cortes", + ]; + + return ( +
+

Pagamento realizado com sucesso!

+

+ Obrigado por assinar o plano Content Creator! Você já pode acessar seus conteúdos e ferramentas. +

+ +
+

Recursos do Plano:

+
    + {contentCreatorFeatures.map((feature, idx) => ( +
  • {feature}
  • + ))} +
+
+ + + Ir para Dashboard + +
+ ); +} diff --git a/Front-End/src/pages/Invoices.tsx b/Front-End/src/pages/Invoices.tsx index abd0e51c..ef79d1aa 100644 --- a/Front-End/src/pages/Invoices.tsx +++ b/Front-End/src/pages/Invoices.tsx @@ -76,18 +76,23 @@ const InvoicesPage = () => { } async function handleDownload(invoice: Invoice) { - if (invoice.pdfUrl) { - window.open(invoice.pdfUrl, '_blank') - return - } try { const res = await fetch(`${backend}/api/invoices/${invoice.id}/download`, { method: 'GET', - headers: { - 'X-API-TOKEN': accessToken - } + headers: { 'X-API-TOKEN': accessToken } }) - if (!res.ok) throw new Error('Falha ao baixar fatura') + + if (!res.ok) { + const body = await res.json().catch(() => ({})) + if (body.pdfUrl) { + // redirecionamento para URL externa + window.open(body.pdfUrl, '_blank') + return + } + throw new Error(body.error || 'Falha ao baixar fatura') + } + + // Resposta é PDF direto const blob = await res.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') @@ -100,6 +105,7 @@ const InvoicesPage = () => { } catch (err: any) { setError(err.message || 'Erro ao baixar fatura') } + } async function handleView(invoice: Invoice) { diff --git a/Front-End/src/pages/Login.tsx b/Front-End/src/pages/Login.tsx index 55d4e90c..e1d1d888 100644 --- a/Front-End/src/pages/Login.tsx +++ b/Front-End/src/pages/Login.tsx @@ -2,18 +2,15 @@ import React, { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; -import { Mail, Lock, LogIn, UserPlus, Loader2 } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Mail, Lock, LogIn, Loader2 } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '@/contexts/AuthContext'; const LoginForm: React.FC = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [isRegistering, setIsRegistering] = useState(false); const [access_token_fallback, setaccess_token_fallback] = useState(''); - // Melhoria de UX: estados para loading e erro const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -27,10 +24,10 @@ const LoginForm: React.FC = () => { if (!email || !password) return; setIsLoading(true); setError(null); - const access_token = localStorage.getItem("access_token") + const access_token = localStorage.getItem("access_token"); try { - const response = await fetch(`${backendUrl}/api/login`, { + const response = await fetch(`${backendUrl}/api/login?email=${email}&password=${password}`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -38,7 +35,6 @@ const LoginForm: React.FC = () => { }, }); - // debug rápido const result = await response.json(); console.log('login response status', response.status, result); @@ -46,7 +42,6 @@ const LoginForm: React.FC = () => { throw new Error(result.message || 'Erro no login'); } - // usa o token direto do result para salvar no localStorage const token = result.acess_token; if (!token) throw new Error('Resposta sem access_token'); @@ -62,9 +57,8 @@ const LoginForm: React.FC = () => { localStorage.setItem('user_senha', email); setaccess_token_fallback(token); - - console.log('Token access_token:', token) - console.log('Token access_token_fallback:', token) + console.log('Token access_token:', token); + console.log('Token access_token_fallback:', token); login(token); navigate(`/home`); @@ -76,100 +70,52 @@ const LoginForm: React.FC = () => { } }; - const handleRegister = async (e: React.FormEvent) => { - e.preventDefault(); - if (password !== confirmPassword) { - setError('As senhas não coincidem.'); - return; - } - - setIsLoading(true); - setError(null); - - try { - const response = await fetch(`${backendUrl}/api/register`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }), - }); - - const result = await response.json(); - console.log('register response', response.status, result); - - if (!response.ok) { - throw new Error(result.message || 'Erro no registro'); - } - - // usa o token direto do result para salvar no localStorage - const token = result.acess_token; - if (!token) throw new Error('Resposta sem access_token'); - - - localStorage.setItem('acess_token', token); - localStorage.setItem('access_token', token); - localStorage.setItem('user_email', email); - localStorage.setItem('user_senha', email); - setaccess_token_fallback(token); - - - console.log('Token access_token:', token) - console.log('Token access_token_fallback:', token) - - - setIsRegistering(false); - - } catch (err: any) { - setError(err.message || 'Erro no registro'); - } finally { - setIsLoading(false); - } - }; - const toggleMode = () => { - setIsRegistering(!isRegistering); - setError(null); // Limpa erros ao trocar de modo - }; - return (

PR AI

- {isRegistering ? "Crie sua conta para começar" : "Bem-vindo de volta! Faça login para continuar"} + Bem-vindo de volta! Faça login para continuar

- - {isRegistering ? 'Criar Conta' : 'Login'} - + Login -
+
- setEmail(e.target.value)} required disabled={isLoading} /> + setEmail(e.target.value)} + required + disabled={isLoading} + />
- setPassword(e.target.value)} required disabled={isLoading} /> + setPassword(e.target.value)} + required + disabled={isLoading} + />
- {isRegistering && ( -
- - setConfirmPassword(e.target.value)} required disabled={isLoading} /> -
- )} - {error && (

{error}

)} @@ -178,18 +124,11 @@ const LoginForm: React.FC = () => { {isLoading ? ( ) : ( - isRegistering ? : + )} - {isRegistering ? 'Registrar' : 'Entrar'} + Entrar - -

- {isRegistering ? 'Já tem uma conta?' : 'Não tem uma conta?'} - -

@@ -197,4 +136,4 @@ const LoginForm: React.FC = () => { ); }; -export default LoginForm; \ No newline at end of file +export default LoginForm; diff --git a/Front-End/src/pages/NotFound.tsx b/Front-End/src/pages/NotFound.tsx index 46ec2ac1..563dad7c 100644 --- a/Front-End/src/pages/NotFound.tsx +++ b/Front-End/src/pages/NotFound.tsx @@ -1,16 +1,20 @@ -import { useLocation } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { useEffect } from "react"; import { Button } from "@/components/ui/button"; const NotFound = () => { const location = useLocation(); + const navigate = useNavigate(); useEffect(() => { console.error( "404 Error: User attempted to access non-existent route:", location.pathname ); - }, [location.pathname]); + + // Redireciona automaticamente para a home + navigate("/"); + }, [location.pathname, navigate]); return (
@@ -23,9 +27,7 @@ const NotFound = () => {

diff --git a/Front-End/src/pages/SignupCheckout.tsx b/Front-End/src/pages/SignupCheckout.tsx new file mode 100644 index 00000000..7fb036ab --- /dev/null +++ b/Front-End/src/pages/SignupCheckout.tsx @@ -0,0 +1,166 @@ +// src/components/SignupCheckout.tsx +import React, { useState } from "react"; +import { loadStripe } from "@stripe/stripe-js"; +import { useSearchParams } from "react-router-dom"; + +const STRIPE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || ""; +const stripePromise = STRIPE_KEY ? loadStripe(STRIPE_KEY) : Promise.resolve(null); + +export default function SignupCheckout(): JSX.Element { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const [searchParams] = useSearchParams(); + const plan = searchParams.get("plan") || "Premium"; + const billingCycleParam = searchParams.get("billing") as "monthly" | "annual" | null; + const [billingCycle] = useState<"monthly" | "annual">(billingCycleParam || "monthly"); + + const priceParam = searchParams.get("price"); + const priceAmount = priceParam ? parseFloat(priceParam) : billingCycle === "annual" ? 88 : 8; + const priceLabel = billingCycle === "annual" ? `$${priceAmount} / ano` : `$${priceAmount} / mês`; + + const VITE_API_URL = import.meta.env.VITE_BACK_END || ""; + + function validateEmail(e: string) { + return /\S+@\S+\.\S+/.test(e); + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + + if (!validateEmail(email)) { + setError("Informe um email válido."); + return; + } + if (password.length < 4) { + setError("Senha precisa ter 4 caracteres ou mais."); + return; + } + if (!VITE_API_URL) { + setError("Configuração de API ausente. Contate o time."); + return; + } + + setLoading(true); + try { + const resp = await fetch(`${VITE_API_URL}/api/billing/checkout`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email, + password, + plan, + billingCycle, + }), + }); + + const body = await resp.json().catch(() => ({})); + if (!resp.ok) throw new Error(body?.error || `Erro do servidor (${resp.status})`); + + if (body.sessionId) { + const stripe = await stripePromise; + if (stripe) { + const result = await stripe.redirectToCheckout({ sessionId: body.sessionId }); + if ((result as any).error) { + setError((result as any).error.message || "Erro ao redirecionar para o Stripe."); + } + } else { + setError("Stripe não configurado no frontend."); + } + } else { + setError("Resposta inesperada do servidor."); + } + } catch (err: any) { + setError(err?.message || "Erro ao criar sessão."); + } finally { + setLoading(false); + } + } + + return ( +
+ {/* Header */} +
+

Finalizar Assinatura

+

+ Você selecionou o plano {plan} —{" "} + {priceLabel} +

+
+ + {/* Form */} +
+
+ + setEmail(e.target.value)} + required + placeholder="seu@exemplo.com" + /> +
+ +
+ + setPassword(e.target.value)} + required + minLength={4} + placeholder="Mínimo 4 caracteres" + /> +

Mínimo 4 caracteres

+
+ + {/* CTA */} +
+ +

+ Você será redirecionado para o checkout seguro do Stripe. +

+
+ + {error && ( +
+ {error} +
+ )} +
+
+ ); +} diff --git a/build.py b/build.py index 027485d3..5e17b084 100644 --- a/build.py +++ b/build.py @@ -10,5 +10,5 @@ def executar_comando(comando): subprocess.run(comando, shell=True) -executar_comando("docker-compose up --build -d ") +executar_comando("docker-compose up --build -d pipeline-api") diff --git a/docker-compose.yml b/docker-compose.yml index 8f82c231..4d746929 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -89,12 +89,13 @@ services: privileged: true volumes: - ./Back-End:/app + - Invoices:/app/Invoices - /var/run/docker.sock:/var/run/docker.sock restart: always ports: - - "5920:5920" + - "5910:5910" command: > - sh -c "uvicorn api:asgi_app --host 0.0.0.0 --port 5920" + sh -c "uvicorn api:asgi_app --host 0.0.0.0 --port 5910" environment: - FLASK_ENV=development - DATABASE_URL=postgresql://postgres:postgres@meu_postgres2:5432/meubanco @@ -118,4 +119,5 @@ volumes: mongodb_data: logger_data: redis_data: - npm-modules: \ No newline at end of file + npm-modules: + Invoices: \ No newline at end of file