diff --git a/Back-End/Workflows/pr.yml b/.github/workflows/pr.yml similarity index 58% rename from Back-End/Workflows/pr.yml rename to .github/workflows/pr.yml index 80dbfdea..c8b787f5 100644 --- a/Back-End/Workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,15 +8,15 @@ jobs: call-external-service: runs-on: ubuntu-latest steps: - - name: Chamar API para processar PR + - name: Chamar API PR AI run: | curl -X POST \ -H "Content-Type: application/json" \ - -H "X-API-TOKEN: ${{ secrets.GITHUB_TOKEN }}" \ + -H "X-API-TOKEN: ${{ secrets.PRAI_API_TOKEN }}" \ -d '{ "repository": "${{ github.repository }}", "pr_number": ${{ github.event.pull_request.number }}, - "pr_url": "${{ github.event.pull_request.url }}", - "diff_url": "${{ github.event.pull_request.diff_url }}" + "email": ${{ secrets.email }}, + "password": "${{ secrets.password }}", }' \ - https://b70c040bdbc5.ngrok-free.app/api/prai/gen + https://api.softwareai.site/api/prai/gen diff --git a/Back-End/Models/postgreSQL.py b/Back-End/Models/postgreSQL.py index 56229ed7..1f0f33ba 100644 --- a/Back-End/Models/postgreSQL.py +++ b/Back-End/Models/postgreSQL.py @@ -3,7 +3,8 @@ import bcrypt from datetime import datetime, timedelta import secrets - +import json +from sqlalchemy import Numeric TOKEN_DEFAULT_EXPIRES_DAYS = 30 @@ -96,3 +97,39 @@ class SystemSettings(db.Model): enable_logging = db.Column(db.Boolean, default=True) log_level = db.Column(db.String(50), default='INFO') updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + +class Invoice(db.Model): + __tablename__ = 'invoices' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) + number = db.Column(db.String(64), nullable=False) # número/identificador da fatura + date = db.Column(db.DateTime, default=datetime.utcnow) + amount = db.Column(Numeric(12, 2), nullable=False, default=0.0) + currency = db.Column(db.String(8), default='BRL') + status = db.Column(db.String(32), default='pending') # paid, pending, failed + plan_name = db.Column(db.String(128), nullable=True) + pdf_path = db.Column(db.String(500), nullable=True) # path relativo em ./invoices/ ou None + pdf_url = db.Column(db.String(1000), nullable=True) # opcional: url externa se armazenada em S3 etc. + lines = db.Column(db.Text, nullable=True) # JSON serializado com itens [{description, qty, price}, ...] + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def to_dict(self, include_lines=False): + data = { + "id": str(self.id), + "number": self.number, + "date": self.date.isoformat() if self.date else None, + "amount": float(self.amount) if self.amount is not None else 0.0, + "currency": self.currency, + "status": self.status, + "planName": self.plan_name, + "pdfUrl": self.pdf_url or (f"/api/invoices/{self.id}/download" if self.pdf_path else None) + } + if include_lines: + try: + data["lines"] = json.loads(self.lines) if self.lines else [] + except Exception: + data["lines"] = [] + return data diff --git a/Back-End/Modules/Resolvers/user_plan.py b/Back-End/Modules/Resolvers/user_plan.py deleted file mode 100644 index effe4c6f..00000000 --- a/Back-End/Modules/Resolvers/user_plan.py +++ /dev/null @@ -1,48 +0,0 @@ - -from Modules.Resolvers.user_identifier import auth_user, require_user_token, resolve_user_identifier - -def user_plan_limit(email, password, logs_collection, app): - - user, _, status = auth_user(email, password, logs_collection, app) - - if status != "success" or not user: - return False - - numeric_user_id = user.id - plan_name = user.plan_name - if plan_name == "Free": - payload = { - 'price': 0, - 'limit_monthly_tokens': 300000, - 'features': [ - 'PR basic automation', - '5 - 10 PRs/mo', - 'Logs basic' - ] - } - elif plan_name == "Premium": - payload = { - 'price': 15, - 'limit_monthly_tokens': 3000000, - 'features': [ - 'PR Premium automation', - '20 - 40 PRs/mo', - 'Logs advanced', - 'API access' - ] - } - - elif plan_name == "Pro": - payload = { - 'price': 29, - 'limit_monthly_tokens': 10000000, - 'features': [ - 'Everything from Premium', - '60 - 90 PRs/mo', - 'Git Context Layer', - 'Auto-Commit Intelligence', - 'Smart Threshold Detection', - 'Context-Aware Messages' - ] - } - return payload \ No newline at end of file diff --git a/Back-End/TestDiscovery/seed_invoices_for_freitas.md b/Back-End/TestDiscovery/seed_invoices_for_freitas.md new file mode 100644 index 00000000..e03361cc --- /dev/null +++ b/Back-End/TestDiscovery/seed_invoices_for_freitas.md @@ -0,0 +1,47 @@ +docker exec -i meu_postgres2 psql -U postgres -d meubanco <<'SQL' +-- seed_invoices_for_freitas.sql +BEGIN; + +-- 1) garante que o usuário exista (insere se não existir) e retorna id em CTE +WITH upsert_user AS ( + INSERT INTO users (email, username, password_hash, created_at, plan_name, limit_monthly_tokens, tokens_used, acess_token, expires_at) + VALUES ( + 'freitasalexandre815@gmail.com', + 'freitasalexandre', + '', + NOW(), + 'Pro', + 1000000, + 1234, + 'testtoken-inv-5000', + NOW() + INTERVAL '30 days' + ) + ON CONFLICT (email) DO UPDATE + SET username = EXCLUDED.username + RETURNING id +), uid AS ( + SELECT id FROM upsert_user + UNION ALL + SELECT id FROM users WHERE email = 'freitasalexandre815@gmail.com' LIMIT 1 +) + +-- 2) Insere 3 faturas somente se não existirem com o mesmo número para esse usuário +INSERT INTO invoices (user_id, number, date, amount, currency, status, plan_name, pdf_path, pdf_url, lines, created_at, updated_at) +SELECT uid.id, v.number, v.date, v.amount, v.currency, v.status, v.plan_name, v.pdf_path, v.pdf_url, v.lines, v.created_at, v.updated_at +FROM uid +CROSS JOIN ( + VALUES + ('INV-2025-0001', NOW() - INTERVAL '10 days', 49.90, 'BRL', 'paid', 'Pro', 'invoice-2025-0001.pdf', NULL, '[{"description":"Assinatura mensal Pro","qty":1,"price":49.90}]', NOW() - INTERVAL '10 days', NOW() - INTERVAL '10 days'), + ('INV-2025-0002', NOW() - INTERVAL '5 days', 49.90, 'BRL', 'pending', 'Pro', NULL, NULL, '[{"description":"Renovação pendente - Pro","qty":1,"price":49.90}]', NOW() - INTERVAL '5 days', NOW() - INTERVAL '5 days'), + ('INV-2025-0003', NOW() - INTERVAL '30 days', 249.00, 'BRL', 'paid', 'Business', NULL, 'https://example-bucket.s3.amazonaws.com/invoice-2025-0003.pdf', '[{"description":"Assinatura anual Business","qty":1,"price":249.00}]', NOW() - INTERVAL '30 days', NOW() - INTERVAL '30 days') +) AS v(number, date, amount, currency, status, plan_name, pdf_path, pdf_url, lines, created_at, updated_at) +WHERE NOT EXISTS ( + SELECT 1 FROM invoices i WHERE i.number = v.number AND i.user_id = uid.id +); + +-- 3) Ajusta sequences +SELECT setval(pg_get_serial_sequence('users','id'), (SELECT COALESCE(MAX(id),1) FROM users)); +SELECT setval(pg_get_serial_sequence('invoices','id'), (SELECT COALESCE(MAX(id),1) FROM invoices)); + +COMMIT; +SQL diff --git a/Back-End/Workflows/PullRequest/pr.yml b/Back-End/Workflows/PullRequest/pr.yml new file mode 100644 index 00000000..c8b787f5 --- /dev/null +++ b/Back-End/Workflows/PullRequest/pr.yml @@ -0,0 +1,22 @@ +name: Processar Pull Request + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + call-external-service: + runs-on: ubuntu-latest + steps: + - name: Chamar API PR AI + run: | + curl -X POST \ + -H "Content-Type: application/json" \ + -H "X-API-TOKEN: ${{ secrets.PRAI_API_TOKEN }}" \ + -d '{ + "repository": "${{ github.repository }}", + "pr_number": ${{ github.event.pull_request.number }}, + "email": ${{ secrets.email }}, + "password": "${{ secrets.password }}", + }' \ + https://api.softwareai.site/api/prai/gen diff --git a/Back-End/api.py b/Back-End/api.py index 14b5f679..5cca2b98 100644 --- a/Back-End/api.py +++ b/Back-End/api.py @@ -4,6 +4,8 @@ import requests import json import logging +from dotenv import load_dotenv + from bson.json_util import dumps from datetime import datetime, timedelta, timezone @@ -11,13 +13,15 @@ from flask_cors import CORS from asgiref.wsgi import WsgiToAsgi from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity -from dotenv import load_dotenv +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address from Models.postgreSQL import * from Modules.Resolvers.pr_process import process_pull_request from Modules.Resolvers.user_identifier import auth_user, require_user_token, resolve_user_identifier from Modules.Geters.systemsettings import * + from Models.mongoDB import ( Log, AuditTrail, @@ -36,6 +40,10 @@ load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), 'keys.env')) +INVOICES_DIR = os.path.join(os.path.dirname(__file__), 'invoices') +os.makedirs(INVOICES_DIR, exist_ok=True) + + app = Flask(__name__) asgi_app = WsgiToAsgi(app) @@ -51,11 +59,17 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False jwt = JWTManager(app) -# CORS(app, resources={ -# r"/api/*": { -# "origins": os.getenv('FRONTEND_ORIGINS', '*').split(',') -# } -# }) +limiter = Limiter( + get_remote_address, + app=app, + default_limits=[] +) + +CORS(app, resources={ + r"/api/*": { + "origins": os.getenv('FRONTEND_ORIGINS', '*').split(',') + } +}) db.init_app(app) @app.route('/') @@ -66,7 +80,66 @@ def index(): "database": "PostgreSQL + MongoDB", "status": "running" }) + +def get_plans_data(): + return { + "Free": { + 'price': 0, + 'limit_monthly_tokens': 300000, + 'features': [ + 'PR basic automation', + '5 - 10 PRs/mo', + 'Logs basic' + ] + }, + "Premium": { + 'price': 15, + 'limit_monthly_tokens': 3000000, + 'features': [ + 'PR Premium automation', + '20 - 40 PRs/mo', + 'Logs advanced', + 'API access' + ] + }, + "Pro": { + 'price': 29, + 'limit_monthly_tokens': 10000000, + 'features': [ + 'Everything from Premium', + '60 - 90 PRs/mo', + 'Git Context Layer', + 'Auto-Commit Intelligence', + 'Smart Threshold Detection', + 'Context-Aware Messages' + ] + } + } +@app.route('/api/public/plans-features', methods=['GET']) +@limiter.limit("5 per minute") +def public_plans_features(): + return jsonify({ + "message": "Lista pública de planos e features", + "payload": get_plans_data() + }), 200 + + +@app.route('/api/plans-features//', methods=['GET']) +def user_plan_limit(email, password): + user, _, status = auth_user(email, password, logs_collection, app) + if status != "success" or not user: + return jsonify({"error": "Credenciais inválidas"}), 401 + plans = get_plans_data() + plan_name = user.plan_name + + if plan_name not in plans: + return jsonify({"error": "Plano não encontrado"}), 404 + + return jsonify({ + "message": "Plano do usuário", + "payload": plans[plan_name], + }), 200 @app.route('/api/register', methods=['POST']) def register(): @@ -104,7 +177,7 @@ def register(): @app.route('/api/login', methods=['POST']) @require_user_token(optional=True) def login(): - data = request.get_json() or {} + data = request.get_json() email = data.get("email") password = data.get("password") try: @@ -123,7 +196,7 @@ def login(): log_action(logs_collection, 'login_success', {'username': email}, user=user.id) return jsonify({ "message": f"Bem-vindo, {user.email}!", - "access_token": access_token_to_return, + "access_token": user.acess_token, "user_id": user.id, "plan_name": user.plan_name, "limit_monthly_tokens": user.limit_monthly_tokens, @@ -781,6 +854,7 @@ def get_pull_request_details(pr_id): 'author': author, 'aiGeneratedContent': ai_generated_content, 'originalDiff': original_diff, + 'total_tokens': pr.total_tokens, 'errorMessage': None if pr.status != 'error' else f'Error processing PR #{pr.pr_number}' } @@ -976,78 +1050,6 @@ def get_dashboard_data(): return jsonify({'error': 'Failed to fetch dashboard data'}), 500 -@app.route('/api/reprocess-pr/', methods=['POST']) -@require_user_token(optional=False) -def reprocess_pr(pr_number): - """Reprocessar um Pull Request específico""" - data = request.get_json() - user_email = data.get("email") - user_senha = data.get("password") - redo_merge = data.get("redo_merge") - - user, _, status = auth_user(user_email, user_senha, logs_collection, app) - - if status != "success" or not user: - return jsonify({"error": "Usuário não autenticado ou inválido"}), 401 - - numeric_user_id = user.id - - model = "gpt-5-nano" - GITHUB_TOKEN, OPENAI_API_KEY, GITHUB_SECRET, REPOSITORY_NAME = get_tokens(numeric_user_id, log_action, logs_collection, SystemSettings, db) - thread = threading.Thread(target=process_pull_request, args=( - app, - numeric_user_id, - GITHUB_TOKEN, - OPENAI_API_KEY, - logs_collection, - pr_number, - REPOSITORY_NAME, - model, - redo_merge, - )) - thread.daemon = True - thread.start() - - return jsonify({ - 'message': 'Processing started', - 'pr_number': pr_number, - 'triggered_by': numeric_user_id - }), 202 - -@app.route('/api/prai/gen', methods=['POST']) -def prai(): - model = "gpt-5-nano" - payload = request.get_json() - user_id = request.args.get("user_id") or (request.get_json(silent=True) or {}).get("user_id") - if not user_id: - return jsonify({"error": "user_id obrigatório"}), 400 - - GitToken = request.headers.get('Bearer') - if not GitToken: - return jsonify({"error": "GitToken obrigatório"}), 400 - - user = resolve_user_identifier(user_id) - if not user: - return jsonify({"error": "Usuário não encontrado."}), 404 - - numeric_user_id = user.id - GITHUB_TOKEN, OPENAI_API_KEY, GITHUB_SECRET, REPOSITORY_NAME = get_tokens(numeric_user_id, log_action, logs_collection, SystemSettings, db) - repository = payload["repository"] - pr_number = payload["pr_number"] - threading.Thread(target=process_pull_request, args=( - app, - numeric_user_id, - GitToken, - OPENAI_API_KEY, - logs_collection, - pr_number, - repository, - model, - )).start() - return 'Processamento do Pull Request iniciado', 202 - - - @app.route('/api/workflows', methods=['GET']) @require_user_token(optional=False) def list_workflows(): @@ -1056,7 +1058,6 @@ def list_workflows(): Retorna JSON: { "workflows": [ { id, name, category, createdAt, yaml, git }, ... ] } """ try: - # obter credenciais do query params ou body (compatível com frontend) user_email = request.args.get("email") or (request.json and request.json.get("email")) user_senha = request.args.get("password") or (request.json and request.json.get("password")) @@ -1066,8 +1067,9 @@ def list_workflows(): numeric_user_id = user.id - # diretório de workflows — configurável via env - workflows_dir = os.getenv('WORKFLOWS_PATH', os.path.join(os.path.dirname(__file__), 'workflows')) + # diretório de workflows + workflows_dir = os.path.join(os.path.dirname(__file__), 'Workflows') + workflows = [] @@ -1106,6 +1108,241 @@ def list_workflows(): log_action(logs_collection, 'workflows_list_error', {'error': str(e)}, user=(getattr(g, 'current_user', None) or None), level='error') return jsonify({'error': 'Failed to list workflows', 'detail': str(e)}), 500 +@app.route('/api/myaccount', methods=['GET']) +@require_user_token(optional=False) +def my_account(): + """ + Retorna informações da conta do usuário autenticado. + Inclui plano, limites e status de expiração. + """ + try: + user_email = request.args.get("email") or (request.json and request.json.get("email")) + user_senha = request.args.get("password") or (request.json and request.json.get("password")) + + user, _, status = auth_user(user_email, user_senha, logs_collection, app) + if status != "success" or not user: + return jsonify({"error": "Usuário não autenticado ou inválido"}), 401 + + numeric_user_id = user.id + response = { + "user_id": user.id, + "email": user.email, + "planName": user.plan_name, + "planExpiresAt": user.expires_at.isoformat() if user.expires_at else None, + "tokensUsed": user.tokens_used or 0, + "tokenLimit": user.limit_monthly_tokens or 0, + "remainingTokens": (user.limit_monthly_tokens or 0) - (user.tokens_used or 0), + "accessToken": user.acess_token, + "createdAt": user.created_at.isoformat() if user.created_at else None, + } + + log_action(logs_collection, 'myaccount_accessed', response, user=user.id) + return jsonify(response), 200 + + except Exception as e: + log_action(logs_collection, 'myaccount_error', {'error': str(e)}, level='error') + return jsonify({"error": "Erro ao recuperar informações da conta", "detail": str(e)}), 500 + + +@app.route('/api/invoices', methods=['GET']) +@require_user_token(optional=False) +def list_invoices(): + """ + Listar faturas paginadas. + Query params: page, limit, status, q, email, password (compatibilidade com auth_user) + Retorna: { invoices: [...], total: N } + """ + try: + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 12)) + status = request.args.get('status') + q = request.args.get('q', '').strip() + + # autentica (compatível com existing pattern) + user_email = request.args.get("email") or (request.json and request.json.get("email")) + user_senha = request.args.get("password") or (request.json and request.json.get("password")) + user, _, status_auth = auth_user(user_email, user_senha, logs_collection, app) + + if status_auth != "success" or not user: + return jsonify({"error": "Usuário não autenticado ou inválido"}), 401 + + numeric_user_id = user.id + + query = Invoice.query.filter_by(user_id=numeric_user_id) + + if status: + query = query.filter(Invoice.status == status) + + if q: + like = f"%{q}%" + query = query.filter(db.or_(Invoice.number.ilike(like), Invoice.plan_name.ilike(like))) + + total = query.count() + + invoices_page = query.order_by(Invoice.date.desc()).offset((page - 1) * limit).limit(limit).all() + + invoices_data = [inv.to_dict(include_lines=False) for inv in invoices_page] + + log_action(logs_collection, 'invoices_listed', {'count': len(invoices_data), 'page': page}, user=numeric_user_id) + return jsonify({"invoices": invoices_data, "total": total}) + except Exception as e: + log_action(logs_collection, 'invoices_list_error', {'error': str(e)}, user=(getattr(g, 'current_user', None) or None), level='error') + return jsonify({'error': 'Failed to list invoices', 'detail': str(e)}), 500 + + +@app.route('/api/invoices/', methods=['GET']) +@require_user_token(optional=False) +def get_invoice_detail(invoice_id): + """ + Detalhe da fatura (inclui linhas / itens). + """ + try: + user_email = request.args.get("email") or (request.json and request.json.get("email")) + user_senha = request.args.get("password") or (request.json and request.json.get("password")) + user, _, status_auth = auth_user(user_email, user_senha, logs_collection, app) + + if status_auth != "success" or not user: + return jsonify({"error": "Usuário não autenticado ou inválido"}), 401 + + numeric_user_id = user.id + + inv = Invoice.query.filter_by(id=invoice_id, user_id=numeric_user_id).first() + if not inv: + return jsonify({'error': 'Invoice not found'}), 404 + + inv_data = inv.to_dict(include_lines=True) + + log_action(logs_collection, 'invoice_detail_accessed', {'invoice_id': invoice_id}, user=numeric_user_id) + return jsonify(inv_data) + except Exception as e: + log_action(logs_collection, 'invoice_detail_error', {'error': str(e), 'invoice_id': invoice_id}, user=(getattr(g, 'current_user', None) or None), level='error') + return jsonify({'error': 'Failed to fetch invoice detail', 'detail': str(e)}), 500 + + +@app.route('/api/invoices//download', methods=['GET']) +@require_user_token(optional=False) +def download_invoice(invoice_id): + """ + Download do PDF da fatura. + Se Invoice.pdf_url estiver setado como URL externa, redireciona para ela. + Se Invoice.pdf_path estiver configurado, serve o arquivo do diretório INVOICES_DIR. + """ + try: + user_email = request.args.get("email") or (request.json and request.json.get("email")) + user_senha = request.args.get("password") or (request.json and request.json.get("password")) + user, _, status_auth = auth_user(user_email, user_senha, logs_collection, app) + + if status_auth != "success" or not user: + return jsonify({"error": "Usuário não autenticado ou inválido"}), 401 + + numeric_user_id = user.id + + inv = Invoice.query.filter_by(id=invoice_id, user_id=numeric_user_id).first() + if not inv: + return jsonify({'error': 'Invoice not found'}), 404 + + # Prioriza URL externa + if inv.pdf_url: + log_action(logs_collection, 'invoice_download_redirect', {'invoice_id': invoice_id, 'url': inv.pdf_url}, user=numeric_user_id) + return jsonify({'pdfUrl': inv.pdf_url}), 200 + + # Se tiver path relativo, tenta servir o arquivo + if inv.pdf_path: + # garante que não escape o diretório + file_path = os.path.join(INVOICES_DIR, inv.pdf_path) + if not os.path.exists(file_path): + log_action(logs_collection, 'invoice_download_missing_file', {'invoice_id': invoice_id, 'path': file_path}, user=numeric_user_id, level='warning') + return jsonify({'error': 'Arquivo de fatura não encontrado no servidor'}), 404 + + # usa send_file com attachment_filename (flask >=2) + try: + log_action(logs_collection, 'invoice_download_served', {'invoice_id': invoice_id, 'file': inv.pdf_path}, user=numeric_user_id) + return send_file(file_path, mimetype='application/pdf', as_attachment=True, download_name=f"invoice-{inv.number}.pdf") + except Exception as e: + log_action(logs_collection, 'invoice_download_error', {'invoice_id': invoice_id, 'error': str(e)}, user=numeric_user_id, level='error') + return jsonify({'error': 'Erro ao servir arquivo de fatura', 'detail': str(e)}), 500 + + # nenhum arquivo nem url configurado + return jsonify({'error': 'Nenhum arquivo de fatura disponível'}), 404 + + except Exception as e: + log_action(logs_collection, 'invoice_download_exception', {'error': str(e), 'invoice_id': invoice_id}, user=(getattr(g, 'current_user', None) or None), level='error') + return jsonify({'error': 'Failed to download invoice', 'detail': str(e)}), 500 + + + +@app.route('/api/reprocess-pr/', methods=['POST']) +@require_user_token(optional=False) +def reprocess_pr(pr_number): + """Reprocessar um Pull Request específico""" + data = request.get_json() + user_email = data.get("email") + user_senha = data.get("password") + redo_merge = data.get("redo_merge") + + user, _, status = auth_user(user_email, user_senha, logs_collection, app) + + if status != "success" or not user: + return jsonify({"error": "Usuário não autenticado ou inválido"}), 401 + + numeric_user_id = user.id + + model = "gpt-5-nano" + GITHUB_TOKEN, OPENAI_API_KEY, GITHUB_SECRET, REPOSITORY_NAME = get_tokens(numeric_user_id, log_action, logs_collection, SystemSettings, db) + thread = threading.Thread(target=process_pull_request, args=( + app, + numeric_user_id, + GITHUB_TOKEN, + OPENAI_API_KEY, + logs_collection, + pr_number, + REPOSITORY_NAME, + model, + redo_merge, + )) + thread.daemon = True + thread.start() + + return jsonify({ + 'message': 'Processing started', + 'pr_number': pr_number, + 'triggered_by': numeric_user_id + }), 202 + +@app.route('/api/prai/gen', methods=['POST']) +@require_user_token(optional=False) +def prai(): + data = request.get_json() + user_email = data.get("email") + user_senha = data.get("password") + repository = data.get("repository") + pr_number = data.get("pr_number") + + user, _, status = auth_user(user_email, user_senha, logs_collection, app) + + if status != "success" or not user: + return jsonify({"error": "Usuário não autenticado ou inválido"}), 401 + + numeric_user_id = user.id + model = "gpt-5-nano" + GITHUB_TOKEN, OPENAI_API_KEY, GITHUB_SECRET, REPOSITORY_NAME = get_tokens(numeric_user_id, log_action, logs_collection, SystemSettings, db) + + threading.Thread(target=process_pull_request, args=( + app, + numeric_user_id, + GITHUB_TOKEN, + OPENAI_API_KEY, + logs_collection, + pr_number, + repository, + model, + )).start() + + return jsonify({ + 'message': 'Processing started', + 'pr_number': pr_number, + 'triggered_by': numeric_user_id + }), 202 # Endpoints nao desenvolvidos e Pendentes diff --git a/Back-End/gen-prai.py b/Back-End/gen-prai.py new file mode 100644 index 00000000..c57773e3 --- /dev/null +++ b/Back-End/gen-prai.py @@ -0,0 +1,36 @@ +import requests +import os +from dotenv import load_dotenv +load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), 'Keys', 'keys.env')) + + +API_URL = "http://localhost:5920/api/prai/gen" + +API_TOKEN = "WBYZIJ3-HCxGandUwpa96l_XlEf1TbYY2oY-4mtL-Hw" +REPOSITORY = "ualers2/SoftwareAI" +PR_NUMBER = 92 +EMAIL = "freitasalexandre815@gmail.com" +PASSWORD = "teste" + +payload = { + "repository": REPOSITORY, + "pr_number": PR_NUMBER, + "email": EMAIL, + "password": PASSWORD, +} + +headers = { + "Content-Type": "application/json", + "X-API-TOKEN": API_TOKEN, +} + +def testar_endpoint(): + try: + response = requests.post(API_URL, json=payload, headers=headers, timeout=30) + print("Status Code:", response.status_code) + print("Resposta JSON:", response.json()) + except Exception as e: + print("Erro ao chamar o endpoint:", str(e)) + +if __name__ == "__main__": + testar_endpoint() diff --git a/Back-End/requirements.txt b/Back-End/requirements.txt index 95cb3e08..9b88a110 100644 --- a/Back-End/requirements.txt +++ b/Back-End/requirements.txt @@ -1,18 +1,23 @@ Flask +flask-limiter +Flask-CORS==4.0.0 +Flask-JWT-Extended==4.6.0 +Flask-SQLAlchemy==3.1.1 +sqlalchemy==2.0.25 + python-dotenv openai-agents asyncio uvicorn asgiref -Flask-CORS==4.0.0 -Flask-JWT-Extended==4.6.0 + psycopg2-binary==2.9.9 pymongo==4.6.1 python-dotenv==1.0.0 requests==2.31.0 bcrypt==4.1.2 -sqlalchemy==2.0.25 -Flask-SQLAlchemy==3.1.1 + + tiktoken diff --git a/Front-End/src/App.tsx b/Front-End/src/App.tsx index 9ca95297..a5a21647 100644 --- a/Front-End/src/App.tsx +++ b/Front-End/src/App.tsx @@ -12,7 +12,11 @@ import Controls from "./pages/Controls"; import Settings from "./pages/Settings"; import NotFound from "./pages/NotFound"; import Login from "./pages/Login"; -import Workflows from "./pages/Workflows"; +import Workflows from "./pages/Workflows"; +import MyAccount from "./pages/MyAccount"; +import BillingPage from "./pages/Billing"; +import InvoicesPage from "./pages/Invoices"; +import Index from "./pages/Landingpage"; import { AuthProvider, useAuth } from "./contexts/AuthContext"; import ProtectedRoute from "./components/ProtectedRoute"; @@ -33,12 +37,18 @@ const AppRoutes = () => { } /> } /> } /> + } /> + } /> + } /> + } /> ) : ( - + + } /> + } /> } /> diff --git a/Front-End/src/assets/hero-image.jpg b/Front-End/src/assets/hero-image.jpg new file mode 100644 index 00000000..10bef923 Binary files /dev/null and b/Front-End/src/assets/hero-image.jpg differ diff --git a/Front-End/src/components/app-sidebar.tsx b/Front-End/src/components/app-sidebar.tsx index 652b3d3a..922db12a 100644 --- a/Front-End/src/components/app-sidebar.tsx +++ b/Front-End/src/components/app-sidebar.tsx @@ -5,7 +5,10 @@ import { GitBranch, ScrollText, Settings, - Zap + Zap, + User, + CreditCard, + FileText } from "lucide-react" import { NavLink, useLocation } from "react-router-dom" @@ -59,6 +62,24 @@ const navigationItems = [ icon: Settings, description: "Configurações do sistema" }, + { + title: "Conta", + url: "/myaccount", + icon: User, + description: "Configurações da conta" + }, + { + title: "Billing", + url: "/billing", + icon: CreditCard, + description: "Configurações de faturamento" + }, + { + title: "Invoices", + url: "/invoices", + icon: FileText, + description: "Relatório de Faturas" + }, ] import { LogOut } from "lucide-react" @@ -88,8 +109,7 @@ export function AppSidebar() { // Limpa tokens ou dados de sessão localStorage.clear() sessionStorage.clear() - // Redireciona para login - navigate("/login") + navigate("/") } return ( diff --git a/Front-End/src/components/cta-section.tsx b/Front-End/src/components/cta-section.tsx new file mode 100644 index 00000000..67c2db68 --- /dev/null +++ b/Front-End/src/components/cta-section.tsx @@ -0,0 +1,88 @@ +import { ArrowRight, Github, Rocket } from "lucide-react"; +import { project } from "@/constants/landingpage.ts"; + +export const CTASection = () => { + const { links, cta } = project[0]; + + return ( +
+ {/* Background Elements */} +
+
+
+
+
+ +
+
+ {/* Icon */} +
+ +
+ + {/* Heading */} +

+ {cta.title.split("Workflow?")[0]} +
+ seu Workflow? +

+ + {/* Description */} +

+ {cta.description} +

+ + {/* Benefits List */} +
+ {cta.benefits.map((benefit, idx) => ( +
+
+ {benefit} +
+ ))} +
+ + {/* CTA Buttons */} + + + {/* Social Proof */} +
+

+ {cta.socialProofTitle} +

+
+ {cta.companies.map((company, idx) => ( +
+ {company} +
+ ))} +
+
+
+
+
+ ); +}; diff --git a/Front-End/src/components/features-section.tsx b/Front-End/src/components/features-section.tsx new file mode 100644 index 00000000..52db8f76 --- /dev/null +++ b/Front-End/src/components/features-section.tsx @@ -0,0 +1,138 @@ +import { + Bot, + Clock, + FileText, + GitBranch, + LineChart, + Shield, + Webhook, + Zap +} from "lucide-react"; + +export const FeaturesSection = () => { + const features = [ + { + icon: Bot, + title: "IA Avançada", + description: "Powered by GPT-5 otimizado para análise de código e geração de documentação técnica precisa.", + gradient: "from-purple-500 to-blue-500" + }, + { + icon: Clock, + title: "90% Economia de Tempo", + description: "Automatize completamente a criação de descrições de PRs. Foque no que realmente importa: o código.", + gradient: "from-green-500 to-teal-500" + }, + { + icon: GitBranch, + title: "Integração GitHub", + description: "Setup em 2 minutos via webhook. Funciona automaticamente com qualquer repositório público ou privado.", + gradient: "from-orange-500 to-red-500" + }, + { + icon: FileText, + title: "Templates Inteligentes", + description: "Descrições estruturadas seguindo as melhores práticas de documentação de engenharia de software.", + gradient: "from-blue-500 to-indigo-500" + }, + { + icon: LineChart, + title: "Analytics & Insights", + description: "Dashboard completo com métricas de produtividade, qualidade de código e performance da equipe.", + gradient: "from-pink-500 to-purple-500" + }, + { + icon: Shield, + title: "Segurança Enterprise", + description: "JWT authentication, logs de auditoria completos e conformidade com padrões de segurança.", + gradient: "from-cyan-500 to-blue-500" + }, + { + icon: Webhook, + title: "Webhooks Customizados", + description: "Integre com Slack, Discord, Teams ou qualquer ferramenta via webhooks personalizáveis.", + gradient: "from-yellow-500 to-orange-500" + }, + { + icon: Zap, + title: "Processamento Instantâneo", + description: "Chunking inteligente para PRs de qualquer tamanho. Processamento em segundos, não minutos.", + gradient: "from-emerald-500 to-green-500" + } + ]; + + return ( +
+ {/* Background Elements */} +
+
+
+
+ +
+ {/* Header */} +
+

+ Recursos que Aceleram +
+ sua Produtividade +

+

+ PR-AI não é apenas outro bot. É a primeira equipe de IA verdadeiramente funcional, + projetada para integrar perfeitamente ao seu workflow existente. +

+
+ + {/* Features Grid */} +
+ {features.map((feature, index) => ( +
+ {/* Icon */} +
+ +
+ + {/* Content */} +

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
+ + {/* Stats Section */} +
+
+
+
90%
+
Menos Tempo
+
na documentação
+
+
+
2min
+
Setup
+
integração completa
+
+
+
24/7
+
Automação
+
sem intervenção
+
+
+
100%
+
Consistência
+
na documentação
+
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/Front-End/src/components/footer.tsx b/Front-End/src/components/footer.tsx new file mode 100644 index 00000000..4b12be64 --- /dev/null +++ b/Front-End/src/components/footer.tsx @@ -0,0 +1,92 @@ +import { Github, Mail, ExternalLink } from "lucide-react"; +import { project } from "@/constants/landingpage.ts"; + +export const Footer = () => { + const github = project[0].links.github + const email = project[0].links.email + + return ( + + ); +}; \ No newline at end of file diff --git a/Front-End/src/components/header.tsx b/Front-End/src/components/header.tsx new file mode 100644 index 00000000..d84127be --- /dev/null +++ b/Front-End/src/components/header.tsx @@ -0,0 +1,98 @@ +import { useState } from "react"; +import { Menu, X, Github } from "lucide-react"; +import { project } from "@/constants/landingpage.ts"; + +export const Header = () => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const { header, links } = project[0]; + + return ( +
+
+
+ {/* Logo */} +
+
+ AI +
+
+

PR-AI

+

by SoftwareAI

+
+
+ + {/* Desktop Navigation */} + + + {/* Desktop Actions */} + + + {/* Mobile Menu Button */} + +
+ + {/* Mobile Menu */} + {isMenuOpen && ( + + )} +
+
+ ); +}; diff --git a/Front-End/src/components/hero-section.tsx b/Front-End/src/components/hero-section.tsx new file mode 100644 index 00000000..13425085 --- /dev/null +++ b/Front-End/src/components/hero-section.tsx @@ -0,0 +1,106 @@ +import { ArrowRight, Github, Zap } from "lucide-react"; +import heroImage from "@/assets/hero-image.jpg"; +import { project } from "@/constants/landingpage.ts"; + +export const HeroSection = () => { + const { github, app } = project[0].links; + + + + return ( +
+ {/* Background Image with Overlay */} +
+ PR-AI Hero Background +
+
+ + {/* Floating Elements */} +
+
+
+
+
+ + {/* Main Content */} +
+
+ {/* Badge */} +
+ + Primeiro Protótipo Funcional +
+ + {/* Main Heading */} +

+ PR-AI +
+ Automatize sua +
+ Documentação +

+ + {/* Subtitle */} +

+ A primeira equipe de IA funcional do SoftwareAI. + Gere descrições de Pull Requests profissionais automaticamente e economize 90% do seu tempo. +

+ + {/* CTA Buttons */} + + + {/* Trust Indicators */} +
+
+
+ +
+
+
90% Economia
+
de tempo na documentação
+
+
+
+
+ +
+
+
GitHub Integration
+
Webhook automático
+
+
+
+
+ +
+
+
IA Avançada
+
GPT-5 otimizado
+
+
+
+
+
+ + {/* Scroll Indicator */} +
+
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/Front-End/src/components/pricing-section.tsx b/Front-End/src/components/pricing-section.tsx new file mode 100644 index 00000000..9bc1a397 --- /dev/null +++ b/Front-End/src/components/pricing-section.tsx @@ -0,0 +1,166 @@ +import { Check, Star, Zap } from "lucide-react"; +import { useEffect, useState } from "react"; +import { project } from "@/constants/landingpage.ts"; + +export const PricingSection = () => { + const [isAnnual, setIsAnnual] = useState(false); + + const backend = import.meta.env.VITE_BACK_END || '' + const [plans, setPlans] = useState([]); + + useEffect(() => { + fetch(`${backend}/api/public/plans-features`) + .then((res) => res.json()) + .then((data) => { + if (data.payload) { + // transforma objeto em array para mapear + const formattedPlans = Object.entries(data.payload).map(([name, info]: any) => ({ + name, + description: `Plano ${name} para diferentes necessidades`, + monthlyPrice: info.price, + annualPrice: Math.round(info.price * 0.83), // desconto ~17% + features: info.features, + isPopular: name === "Premium", // destaque no frontend + })); + setPlans(formattedPlans); + } + }) + .catch((err) => console.error("Erro ao buscar planos:", err)); + }, []); + + return ( +
+
+ {/* Header */} +
+

+ Planos que Escalam +
+ com seu Crescimento +

+

+ Escolha o plano perfeito para sua equipe. Todos os planos incluem 7 dias grátis para testar. +

+ + {/* Toggle Annual/Monthly */} +
+ + Mensal + + + + Anual + + {isAnnual && ( + + -17% Desconto + + )} +
+
+ + {/* Pricing Cards */} +
+ {plans.map((plan, index) => ( +
+ {plan.isPopular && ( +
+
+ + Mais Popular +
+
+ )} + + {/* Plan Header */} +
+

{plan.name}

+

{plan.description}

+
+ + ${isAnnual ? plan.annualPrice : plan.monthlyPrice} + + /mês +
+ {isAnnual && ( +
+ Faturado anualmente (${(isAnnual ? plan.annualPrice : plan.monthlyPrice) * 12}) +
+ )} +
+ + {/* Features */} +
+ {plan.features.map((feature, featureIndex) => ( +
+
+ +
+ {feature} +
+ ))} +
+ + {/* CTA Button */} + + + + {plan.isPopular && ( +
+ + ⚡ Setup em menos de 5 minutos + +
+ )} +
+ ))} +
+ + {/* Enterprise CTA */} +
+
+

+ Precisa de algo Personalizado? +

+

+ Para organizações com necessidades específicas, oferecemos planos enterprise + totalmente customizados com recursos avançados e suporte dedicado. +

+ +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/Front-End/src/constants/landingpage.ts b/Front-End/src/constants/landingpage.ts new file mode 100644 index 00000000..e01cc2cc --- /dev/null +++ b/Front-End/src/constants/landingpage.ts @@ -0,0 +1,50 @@ +export const project = [ +{ + id: 1, + links: { + github: 'https://github.com/ualers2/SoftwareAI', + app: 'https://www.softwareai.site/login', + email: '' + }, + cta: { + title: "Pronto para Revolucionar seu Workflow?", + description: + "Junte-se a centenas de equipes que já economizam horas semanais com PR-AI. Setup em 2 minutos, resultados imediatos.", + benefits: [ + "7 dias grátis", + "Sem cartão de crédito", + "Cancelamento fácil", + "Suporte brasileiro", + ], + socialProofTitle: "Confiado por desenvolvedores em empresas como:", + companies: ["Media Cuts Studio", "Employers AI", "Docshepere", "CodeLab"], + }, + header: { + navigation: [ + { name: "Recursos", href: "#features" }, + { name: "Preços", href: "#pricing" }, + { name: "Documentação", href: "#docs" }, + { name: "Contato", href: "#contact" }, + ], + actions: { + login: { label: "Entrar", href: "/login" }, + signup: { label: "Começar Grátis", href: "/signup" }, + }, + }, + plans: [ + { + name: "Free", + checkout: "https://www.softwareai.site/signup?plan=free" + }, + { + name: "Premium", + checkout: "https://checkout.stripe.com/pay/premium123" + }, + { + name: "Pro", + checkout: "https://checkout.stripe.com/pay/pro123" + } + ], +}, + +]; diff --git a/Front-End/src/pages/Billing.tsx b/Front-End/src/pages/Billing.tsx new file mode 100644 index 00000000..4278a9bf --- /dev/null +++ b/Front-End/src/pages/Billing.tsx @@ -0,0 +1,242 @@ +import React, { useEffect, useState } from 'react' +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' + +type AccountData = { + user_id: number + email: string + planName: string + planExpiresAt: string | null + tokensUsed: number + tokenLimit: number + remainingTokens: number +} + +const samplePlans = [ + { + id: 'free', + name: 'Free', + price: 'R$ 0/mês', + features: ['300k tokens / mês', 'Suporte via docs', 'Limitações básicas'] + }, + { + id: 'pro', + name: 'Pro', + price: 'R$ 49/mês', + features: ['1M tokens / mês', 'Suporte por e-mail', 'Renovação automática'] + }, + { + id: 'business', + name: 'Business', + price: 'R$ 249/mês', + features: ['Tokens ilimitados', 'Suporte prioritário', 'Conta multi-usuário'] + } +] + +const BillingPage = () => { + const [account, setAccount] = useState(null) + const [loading, setLoading] = useState(false) + const [actionLoading, setActionLoading] = useState(null) + const [error, setError] = useState(null) + + const backend = import.meta.env.VITE_BACK_END || '' + const email = localStorage.getItem("user_email") || "" + const password = localStorage.getItem("user_senha") || "" + const accessToken = localStorage.getItem("access_token") || "" + + useEffect(() => { + fetchAccount() + }, []) + + async function fetchAccount() { + setLoading(true) + setError(null) + try { + const res = await fetch(`${backend}/api/myaccount?email=${email}&password=${password}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-API-TOKEN': accessToken + } + }) + if (!res.ok) throw new Error('Falha ao buscar dados da conta') + const data = await res.json() + setAccount({ + user_id: data.user_id, + email: data.email, + planName: data.planName || 'Free', + planExpiresAt: data.planExpiresAt || null, + tokensUsed: data.tokensUsed || 0, + tokenLimit: data.tokenLimit || 0, + remainingTokens: data.remainingTokens || 0 + }) + } catch (err: any) { + setError(err.message || 'Erro desconhecido') + } finally { + setLoading(false) + } + } + + function calcPercent(used: number, limit: number) { + if (limit === 0) return 0 + return Math.min(100, Math.round((used / limit) * 100)) + } + + async function handleRenew() { + if (!account) return + setActionLoading('renew') + try { + const res = await fetch(`${backend}/api/renew-plan`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-TOKEN': accessToken + }, + body: JSON.stringify({ plan: account.planName }) + }) + if (!res.ok) throw new Error('Falha ao renovar o plano') + await fetchAccount() + } catch (err: any) { + setError(err.message || 'Erro na renovação') + } finally { + setActionLoading(null) + } + } + + async function handleSubscribe(planId: string) { + setActionLoading(planId) + try { + const res = await fetch(`${backend}/api/subscribe`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-TOKEN': accessToken + }, + body: JSON.stringify({ planId }) + }) + if (!res.ok) { + const body = await res.json().catch(() => ({})) + throw new Error(body.error || 'Falha ao assinar plano') + } + await fetchAccount() + } catch (err: any) { + setError(err.message || 'Erro ao assinar plano') + } finally { + setActionLoading(null) + } + } + + return ( +
+

Billing — Minha conta

+ + {error && ( +
{error}
+ )} + +
+ {/* Left column: account summary */} +
+ + + Plano atual + + + {loading || !account ? ( +
Carregando...
+ ) : ( + <> +
+
+
{account.planName}
+
Expira em: {account.planExpiresAt ?? 'N/A'}
+
+
+ {account.remainingTokens} tokens restantes +
+
+ +
+
Uso de tokens: {account.tokensUsed} / {account.tokenLimit}
+
+
+
+
+ +
+
+ + )} + + + + + + Tokens consumidos + + + {account ? ( + <> +
{account.tokensUsed}
+
Limite: {account.tokenLimit}
+
+
+
+ + ) : ( +
+ )} + + + + + + Ações + + +
+ + +
+
+
+
+ + {/* Right column: plans */} +
+
+ {samplePlans.map((p) => ( + + + {p.name} + + +
+
{p.price}
+
    + {p.features.map((f, i) => ( +
  • • {f}
  • + ))} +
+
+ +
+ +
+
+
+ ))} +
+
+
+ +
+ ) +} + +export default BillingPage diff --git a/Front-End/src/pages/Index.tsx b/Front-End/src/pages/Index.tsx deleted file mode 100644 index 99637ff1..00000000 --- a/Front-End/src/pages/Index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// Update this page (the content is just a fallback if you fail to update the page) - -const Index = () => { - return ( -
-
-

Welcome to Your Blank App

-

Start building your amazing project here!

-
-
- ); -}; - -export default Index; diff --git a/Front-End/src/pages/Invoices.tsx b/Front-End/src/pages/Invoices.tsx new file mode 100644 index 00000000..a9480b5a --- /dev/null +++ b/Front-End/src/pages/Invoices.tsx @@ -0,0 +1,255 @@ +import React, { useEffect, useState } from 'react' +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Select } from '@/components/ui/select' + +type Invoice = { + id: string + number: string + date: string + amount: number + currency?: string + status: 'paid' | 'pending' | 'failed' + planName?: string + pdfUrl?: string | null + lines?: Array<{ description: string; qty?: number; price: number }> +} + +const InvoicesPage = () => { + const [invoices, setInvoices] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [page, setPage] = useState(1) + const [pageSize] = useState(12) + const [total, setTotal] = useState(0) + const [filterStatus, setFilterStatus] = useState<'all' | Invoice['status']>('all') + const [query, setQuery] = useState('') + const [selectedInvoice, setSelectedInvoice] = useState(null) + + const backend = import.meta.env.VITE_BACK_END || '' + const email = localStorage.getItem("user_email") || "" + const password = localStorage.getItem("user_senha") || "" + const accessToken = localStorage.getItem("access_token") || "" + + useEffect(() => { + fetchInvoices() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page, filterStatus]) + + async function fetchInvoices() { + setLoading(true) + setError(null) + try { + const params = new URLSearchParams() + params.set('page', String(page)) + params.set('limit', String(pageSize)) + params.set('email', String(email)) + params.set('password', String(password)) + if (filterStatus !== 'all') params.set('status', filterStatus) + if (query) params.set('q', query) + + const res = await fetch(`${backend}/api/invoices?${params.toString()}`, { + headers: { + 'Content-Type': 'application/json', + 'X-API-TOKEN': accessToken + } + }) + + if (!res.ok) throw new Error('Falha ao buscar faturas') + + const body = await res.json() + // Espera: { invoices: [...], total: number } + setInvoices(body.invoices || []) + setTotal(body.total || 0) + } catch (err: any) { + setError(err.message || 'Erro desconhecido') + } finally { + setLoading(false) + } + } + + function formatCurrency(v: number, currency = 'BRL') { + try { + return new Intl.NumberFormat('pt-BR', { style: 'currency', currency }).format(v) + } catch { + return `${v}` + } + } + + 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 + } + }) + if (!res.ok) throw new Error('Falha ao baixar fatura') + const blob = await res.blob() + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `invoice-${invoice.number}.pdf` + document.body.appendChild(a) + a.click() + a.remove() + window.URL.revokeObjectURL(url) + } catch (err: any) { + setError(err.message || 'Erro ao baixar fatura') + } + } + + async function handleView(invoice: Invoice) { + const params = new URLSearchParams() + params.set('email', String(email)) + params.set('password', String(password)) + if (!invoice.lines) { + try { + const res = await fetch(`${backend}/api/invoices/${invoice.id}?${params.toString()}`, { + headers: { + 'X-API-TOKEN': accessToken + } + }) + if (res.ok) { + const body = await res.json() + setSelectedInvoice({ ...invoice, ...body }) + return + } + } catch (e) { + } + } + setSelectedInvoice(invoice) + } + + function closeModal() { + setSelectedInvoice(null) + } + + const totalPages = Math.max(1, Math.ceil(total / pageSize)) + + return ( +
+
+

Faturas

+
+ +
+
+ +
+ setQuery(e.target.value)} /> + +
+ +
+
+ + + + Lista de faturas + + + {loading ? ( +
Carregando faturas...
+ ) : error ? ( +
{error}
+ ) : invoices.length === 0 ? ( +
Sem faturas encontradas.
+ ) : ( +
+ + + + + + + + + + + + + {invoices.map(inv => ( + + + + + + + + + ))} + +
#DataPlanoValorStatusAções
{inv.number}{new Date(inv.date).toLocaleString()}{inv.planName || '—'}{formatCurrency(inv.amount, inv.currency || 'BRL')}{inv.status} +
+ + +
+
+ +
+
Mostrando {invoices.length} de {total} faturas
+
+ +
{page} / {totalPages}
+ +
+
+
+ )} +
+
+ + {/* Modal simples de fatura */} + {selectedInvoice && ( +
+
+
+
+

Fatura #{selectedInvoice.number}

+
{new Date(selectedInvoice.date).toLocaleString()}
+
+
+ + +
+
+ +
+
+ Plano: {selectedInvoice.planName || '—'} +
+
+ Valor: {formatCurrency(selectedInvoice.amount, selectedInvoice.currency || 'BRL')} +
+ +
+ Itens: +
    + {selectedInvoice.lines && selectedInvoice.lines.length > 0 ? ( + selectedInvoice.lines.map((l, i) => ( +
  • {l.description} — {l.qty ? `${l.qty} x ` : ''}{formatCurrency(l.price)}
  • + )) + ) : ( +
  • Sem detalhamento disponível.
  • + )} +
+
+
+
+
+ )} +
+ ) +} +export default InvoicesPage diff --git a/Front-End/src/pages/Landingpage.tsx b/Front-End/src/pages/Landingpage.tsx new file mode 100644 index 00000000..cebac923 --- /dev/null +++ b/Front-End/src/pages/Landingpage.tsx @@ -0,0 +1,23 @@ +import { Header } from "@/components/header"; +import { HeroSection } from "@/components/hero-section"; +import { FeaturesSection } from "@/components/features-section"; +import { PricingSection } from "@/components/pricing-section"; +import { CTASection } from "@/components/cta-section"; +import { Footer } from "@/components/footer"; + +const Index = () => { + return ( +
+
+
+ + + + +
+
+
+ ); +}; + +export default Index; diff --git a/Front-End/src/pages/MyAccount.tsx b/Front-End/src/pages/MyAccount.tsx new file mode 100644 index 00000000..7c68493c --- /dev/null +++ b/Front-End/src/pages/MyAccount.tsx @@ -0,0 +1,150 @@ +// webproject\src\components\MyAccount.tsx +import { useEffect, useState } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Badge } from "@/components/ui/badge" +import { Mail, Lock, Key, Zap, Calendar, RefreshCw } from "lucide-react" + +interface AccountData { + email: string + password: string + accessToken: string + planExpiresAt: string + tokensUsed: number + tokenLimit: number + tokenPercentUsed: number +} + +const MyAccount = () => { + const [account, setAccount] = useState({ + email: "", + password: "", + accessToken: "", + planExpiresAt: "N/A", + tokensUsed: 0, + tokenLimit: 0, + tokenPercentUsed: 0, + }) + const [isLoading, setIsLoading] = useState(false) + + const backendUrl = import.meta.env.VITE_BACK_END + const email = localStorage.getItem("user_email") || "" + const password = localStorage.getItem("user_senha") || "" + const accessToken = localStorage.getItem("access_token") || "" + + const fetchAccountData = async () => { + if (!email || !accessToken) return + try { + setIsLoading(true) + const response = await fetch(`${backendUrl}/api/myaccount?email=${email}&password=${password}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-API-TOKEN": accessToken, + }, + }) + + if (!response.ok) { + console.error("Erro ao buscar dados da conta:", response.status) + return + } + + const result = await response.json() + setAccount({ + email, + password, + accessToken, + planExpiresAt: result.planExpiresAt ?? "N/A", + tokensUsed: result.tokensUsed ?? 0, + tokenLimit: result.tokenLimit ?? 0, + tokenPercentUsed: result.tokenPercentUsed ?? 0, + }) + } catch (err) { + console.error("Erro ao carregar conta:", err) + } finally { + setIsLoading(false) + } + } + + useEffect(() => { + fetchAccountData() + }, []) + + return ( +
+
+

Minha Conta

+ +
+ + + + Informações Pessoais + + +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+
+ + + + Plano & Tokens + + +
+ + Expiração do Plano + + {account.planExpiresAt} +
+ +
+ + Tokens Consumidos + + + {account.tokensUsed.toLocaleString()} / {account.tokenLimit.toLocaleString()} ( + {account.tokenPercentUsed}%) + +
+
+
+
+ ) +} + +export default MyAccount diff --git a/README.md b/README.md index 23c070bc..9572147c 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ > > A maneira mais fácil e rápida de experimentar o poder do SoftwareAI é através da nossa plataforma oficial. > -> **Visite [www.softwareai.site](https://www.google.com/search?q=https://www.softwareai.site) para iniciar gratuitamente!** +> **Visite [www.softwareai.site](https://www.softwareai.site) para iniciar gratuitamente!**
-[Plataforma Oficial](https://www.google.com/search?q=https://www.softwareai.site) • [Documentação](https://www.google.com/search?q=%23documenta%C3%A7%C3%A3o) • [Instalação Local]() • [Arquitetura](#) +[Plataforma Oficial](https://www.softwareai.site) • [Documentação](https://www.softwareai.site/docs/api) • [Instalação Local (em breve)]() • [Arquitetura](#️-arquitetura-do-sistema-pr-ai)
diff --git a/build.py b/build.py index e93a2980..027485d3 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 frontend-pipeline") +executar_comando("docker-compose up --build -d ") diff --git a/docker-compose.yml b/docker-compose.yml index 524195af..8f82c231 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,7 +71,7 @@ services: - "4684:4684" restart: always command: > - sh -c "npm ci && npm run build && npx serve -s dist -l 4684" + sh -c "npm run dev -- --port 4684" healthcheck: test: ["CMD", "curl", "-f", "http://127.0.0.1:4684"] interval: 129s