mirror of
https://github.com/hoshikawa2/rfp_response_automation.git
synced 2026-07-10 12:04:20 +00:00
first commit
This commit is contained in:
4
files/modules/users/__init__.py
Normal file
4
files/modules/users/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .routes import users_bp
|
||||
from .model import db
|
||||
|
||||
__all__ = ["users_bp", "db"]
|
||||
50
files/modules/users/db.py
Normal file
50
files/modules/users/db.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import oracledb
|
||||
import json
|
||||
import base64
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
import requests
|
||||
import textwrap
|
||||
import unicodedata
|
||||
from typing import Optional
|
||||
from collections import deque
|
||||
from config_loader import load_config
|
||||
|
||||
def chunk_hash(text: str) -> str:
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
config = load_config()
|
||||
|
||||
# =========================
|
||||
# Oracle Autonomous Configuration
|
||||
# =========================
|
||||
WALLET_PATH = config.wallet_path
|
||||
DB_ALIAS = config.db_alias
|
||||
USERNAME = config.username
|
||||
PASSWORD = config.password
|
||||
os.environ["TNS_ADMIN"] = WALLET_PATH
|
||||
|
||||
_pool = None
|
||||
|
||||
def get_pool():
|
||||
global _pool
|
||||
|
||||
if _pool:
|
||||
return _pool
|
||||
|
||||
_pool = oracledb.create_pool(
|
||||
user=USERNAME,
|
||||
password=PASSWORD,
|
||||
dsn=DB_ALIAS,
|
||||
config_dir=WALLET_PATH,
|
||||
wallet_location=WALLET_PATH,
|
||||
wallet_password=PASSWORD,
|
||||
min=2,
|
||||
max=8,
|
||||
increment=1
|
||||
)
|
||||
|
||||
return _pool
|
||||
72
files/modules/users/email_service.py
Normal file
72
files/modules/users/email_service.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import os
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
from flask import current_app
|
||||
from config_loader import load_config
|
||||
|
||||
config = load_config()
|
||||
API_BASE_URL = f"{config.app_base}:{config.service_port}"
|
||||
|
||||
def send_user_created_email(email, link, name=""):
|
||||
"""
|
||||
DEV -> return link only
|
||||
PROD -> send real email
|
||||
"""
|
||||
|
||||
if config.dev_mode == 1:
|
||||
return link # 👈 só devolve o link
|
||||
|
||||
host = os.getenv("RFP_SMTP_HOST", "localhost")
|
||||
port = int(os.getenv("RFP_SMTP_PORT", 25))
|
||||
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = "Your account has been created"
|
||||
msg["From"] = "noreply@rfp.local"
|
||||
msg["To"] = email
|
||||
|
||||
msg.set_content(f"""
|
||||
Hello {name or email},
|
||||
|
||||
Your account was created.
|
||||
|
||||
Set your password here:
|
||||
{link}
|
||||
""")
|
||||
|
||||
with smtplib.SMTP(host, port) as s:
|
||||
s.send_message(msg)
|
||||
|
||||
return link
|
||||
|
||||
def send_completion_email(email, download_url, job_id):
|
||||
"""
|
||||
DEV -> return download link
|
||||
PROD -> send real email
|
||||
"""
|
||||
|
||||
if config.dev_mode == 1:
|
||||
return download_url # 👈 só devolve o link no DEV
|
||||
|
||||
host = os.getenv("RFP_SMTP_HOST", "localhost")
|
||||
port = int(os.getenv("RFP_SMTP_PORT", 25))
|
||||
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = "Your RFP processing is complete"
|
||||
msg["From"] = "noreply@rfp.local"
|
||||
msg["To"] = email
|
||||
|
||||
msg.set_content(f"""
|
||||
Hello,
|
||||
|
||||
Your RFP Excel processing has finished successfully.
|
||||
|
||||
Download your file here:
|
||||
{download_url}
|
||||
|
||||
Job ID: {job_id}
|
||||
""")
|
||||
|
||||
with smtplib.SMTP(host, port) as s:
|
||||
s.send_message(msg)
|
||||
|
||||
return None
|
||||
27
files/modules/users/model.py
Normal file
27
files/modules/users/model.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from datetime import datetime
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
name = db.Column(db.String(120), nullable=False)
|
||||
email = db.Column(db.String(160), unique=True, nullable=False, index=True)
|
||||
|
||||
role = db.Column(db.String(50), default="app") # app | admin
|
||||
active = db.Column(db.Boolean, default=True)
|
||||
|
||||
password_hash = db.Column(db.String(255))
|
||||
must_change_password = db.Column(db.Boolean, default=True)
|
||||
|
||||
reset_token = db.Column(db.String(255))
|
||||
reset_expire = db.Column(db.DateTime)
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User {self.email}>"
|
||||
157
files/modules/users/routes.py
Normal file
157
files/modules/users/routes.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from modules.core.security import requires_admin_auth
|
||||
|
||||
from .service import (
|
||||
signup_user,
|
||||
list_users as svc_list_users,
|
||||
create_user,
|
||||
update_user,
|
||||
delete_user as svc_delete_user,
|
||||
get_user_by_token,
|
||||
set_password_service
|
||||
)
|
||||
|
||||
from .token_service import generate_token, expiration, is_expired
|
||||
from .email_service import send_user_created_email
|
||||
from config_loader import load_config
|
||||
|
||||
users_bp = Blueprint(
|
||||
"users",
|
||||
__name__,
|
||||
template_folder="../../templates/users"
|
||||
)
|
||||
|
||||
config = load_config()
|
||||
|
||||
|
||||
# =========================
|
||||
# LIST USERS (Oracle)
|
||||
# =========================
|
||||
@users_bp.route("/")
|
||||
@requires_admin_auth
|
||||
def list_users():
|
||||
users = svc_list_users()
|
||||
return render_template("list.html", users=users)
|
||||
|
||||
|
||||
# =========================
|
||||
# PUBLIC SIGNUP (Oracle)
|
||||
# =========================
|
||||
@users_bp.route("/signup", methods=["GET", "POST"])
|
||||
def signup():
|
||||
|
||||
if request.method == "POST":
|
||||
email = request.form.get("email", "").strip()
|
||||
name = request.form.get("name", "").strip()
|
||||
|
||||
try:
|
||||
link = signup_user(email=email, name=name)
|
||||
except Exception as e:
|
||||
flash(str(e), "danger")
|
||||
return render_template("users/signup.html")
|
||||
|
||||
if link and config.dev_mode == 1:
|
||||
flash(f"DEV MODE: password link → {link}", "success")
|
||||
else:
|
||||
flash("User created and email sent", "success")
|
||||
|
||||
return redirect(url_for("users.signup"))
|
||||
|
||||
return render_template("users/signup.html")
|
||||
|
||||
|
||||
# =========================
|
||||
# CREATE USER (Oracle)
|
||||
# =========================
|
||||
@users_bp.route("/new", methods=["GET", "POST"])
|
||||
@requires_admin_auth
|
||||
def new_user():
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
token = generate_token()
|
||||
|
||||
create_user(
|
||||
name=request.form["name"],
|
||||
email=request.form["email"],
|
||||
role=request.form["role"],
|
||||
active="active" in request.form,
|
||||
token=token
|
||||
)
|
||||
|
||||
link = url_for("users.set_password", token=token, _external=True)
|
||||
|
||||
dev_link = send_user_created_email(
|
||||
request.form["email"],
|
||||
link,
|
||||
request.form["name"]
|
||||
)
|
||||
|
||||
flash("User created and email sent", "success")
|
||||
return redirect(url_for("users.list_users"))
|
||||
|
||||
return render_template("form.html", user=None)
|
||||
|
||||
|
||||
# =========================
|
||||
# EDIT USER (Oracle)
|
||||
# =========================
|
||||
@users_bp.route("/edit/<int:user_id>", methods=["GET", "POST"])
|
||||
@requires_admin_auth
|
||||
def edit_user(user_id):
|
||||
|
||||
if request.method == "POST":
|
||||
update_user(
|
||||
user_id=user_id,
|
||||
name=request.form["name"],
|
||||
email=request.form["email"],
|
||||
role=request.form["role"],
|
||||
active="active" in request.form
|
||||
)
|
||||
|
||||
return redirect(url_for("users.list_users"))
|
||||
|
||||
# busca lista inteira e filtra (simples e funciona bem)
|
||||
users = svc_list_users()
|
||||
user = next((u for u in users if u["id"] == user_id), None)
|
||||
|
||||
return render_template("form.html", user=user)
|
||||
|
||||
|
||||
# =========================
|
||||
# DELETE USER (Oracle)
|
||||
# =========================
|
||||
@users_bp.route("/delete/<int:user_id>")
|
||||
@requires_admin_auth
|
||||
def delete_user(user_id):
|
||||
|
||||
svc_delete_user(user_id)
|
||||
return redirect(url_for("users.list_users"))
|
||||
|
||||
|
||||
# =========================
|
||||
# SET PASSWORD (Oracle)
|
||||
# =========================
|
||||
@users_bp.route("/set-password/<token>", methods=["GET", "POST"])
|
||||
def set_password(token):
|
||||
|
||||
user = get_user_by_token(token)
|
||||
|
||||
if not user or is_expired(user["expire"]):
|
||||
return render_template("set_password.html", expired=True)
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
pwd = request.form["password"]
|
||||
pwd2 = request.form["password2"]
|
||||
|
||||
if pwd != pwd2:
|
||||
flash("Passwords do not match")
|
||||
return render_template("set_password.html", expired=False)
|
||||
|
||||
set_password_service(user["id"], pwd)
|
||||
|
||||
flash("Password updated successfully")
|
||||
return redirect("/")
|
||||
|
||||
return render_template("set_password.html", expired=False)
|
||||
204
files/modules/users/service.py
Normal file
204
files/modules/users/service.py
Normal file
@@ -0,0 +1,204 @@
|
||||
#from .model import db, User
|
||||
from .token_service import generate_token, expiration
|
||||
from .email_service import send_user_created_email
|
||||
from config_loader import load_config
|
||||
from .db import get_pool
|
||||
import bcrypt
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
config = load_config()
|
||||
|
||||
def authenticate_user(username: str, password: str):
|
||||
|
||||
print("LOGIN TRY:", username, password)
|
||||
|
||||
sql = """
|
||||
SELECT password_hash
|
||||
FROM app_users
|
||||
WHERE email = :1 \
|
||||
"""
|
||||
|
||||
pool = get_pool()
|
||||
|
||||
with pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, [username])
|
||||
row = cur.fetchone()
|
||||
|
||||
# print("ROW:", row)
|
||||
|
||||
if not row:
|
||||
# print("USER NOT FOUND")
|
||||
return False
|
||||
|
||||
stored_hash = row[0]
|
||||
# print("HASH:", stored_hash)
|
||||
|
||||
ok = check_password_hash(stored_hash, password)
|
||||
|
||||
# print("MATCH:", ok)
|
||||
|
||||
return ok
|
||||
|
||||
def create_user(username: str, password: str):
|
||||
|
||||
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
||||
|
||||
sql = """
|
||||
INSERT INTO app_users (username, password_hash)
|
||||
VALUES (:1, :2) \
|
||||
"""
|
||||
|
||||
pool = get_pool()
|
||||
|
||||
with pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, [username, hashed])
|
||||
conn.commit()
|
||||
|
||||
def _default_name(email: str) -> str:
|
||||
return (email or "").split("@")[0]
|
||||
|
||||
|
||||
def signup_user(email: str, name: str = ""):
|
||||
|
||||
if not email:
|
||||
raise ValueError("Email required")
|
||||
|
||||
email = email.lower().strip()
|
||||
name = name or email.split("@")[0]
|
||||
|
||||
token = generate_token()
|
||||
|
||||
pool = get_pool()
|
||||
|
||||
sql_check = """
|
||||
SELECT id
|
||||
FROM app_users
|
||||
WHERE email = :1 \
|
||||
"""
|
||||
|
||||
sql_insert = """
|
||||
INSERT INTO app_users
|
||||
(name,email,user_role,active,reset_token,reset_expire,must_change_password)
|
||||
VALUES (:1,:2,'user',1,:3,:4,1) \
|
||||
"""
|
||||
|
||||
sql_update = """
|
||||
UPDATE app_users
|
||||
SET reset_token=:1,
|
||||
reset_expire=:2,
|
||||
must_change_password=1
|
||||
WHERE email=:3 \
|
||||
"""
|
||||
|
||||
with pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
|
||||
cur.execute(sql_check, [email])
|
||||
row = cur.fetchone()
|
||||
|
||||
if not row:
|
||||
cur.execute(sql_insert, [name, email, token, expiration()])
|
||||
else:
|
||||
cur.execute(sql_update, [token, expiration(), email])
|
||||
|
||||
conn.commit()
|
||||
|
||||
link = f"{config.app_base}:{config.service_port}/admin/users/set-password/{token}"
|
||||
|
||||
dev_link = send_user_created_email(email, link, name)
|
||||
|
||||
return dev_link or link
|
||||
|
||||
def list_users():
|
||||
sql = """
|
||||
SELECT id, name, email, user_role, active
|
||||
FROM app_users
|
||||
ORDER BY name \
|
||||
"""
|
||||
|
||||
pool = get_pool()
|
||||
|
||||
with pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql)
|
||||
cols = [c[0].lower() for c in cur.description]
|
||||
return [dict(zip(cols, r)) for r in cur.fetchall()]
|
||||
|
||||
def create_user(name, email, role, active, token):
|
||||
sql = """
|
||||
INSERT INTO app_users
|
||||
(name,email,user_role,active,reset_token,reset_expire,must_change_password)
|
||||
VALUES (:1,:2,:3,:4,:5,SYSTIMESTAMP + INTERVAL '1' DAY,1) \
|
||||
"""
|
||||
|
||||
pool = get_pool()
|
||||
|
||||
with pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, [name, email, role, active, token])
|
||||
conn.commit()
|
||||
|
||||
def update_user(user_id, name, email, role, active):
|
||||
sql = """
|
||||
UPDATE app_users
|
||||
SET name=:1, email=:2, user_role=:3, active=:4
|
||||
WHERE id=:5 \
|
||||
"""
|
||||
|
||||
pool = get_pool()
|
||||
|
||||
with pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, [name, email, role, active, user_id])
|
||||
conn.commit()
|
||||
|
||||
def delete_user(user_id):
|
||||
sql = "DELETE FROM app_users WHERE id=:1"
|
||||
|
||||
pool = get_pool()
|
||||
|
||||
with pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, [user_id])
|
||||
conn.commit()
|
||||
|
||||
def get_user_by_token(token):
|
||||
sql = """
|
||||
SELECT id, reset_expire
|
||||
FROM app_users
|
||||
WHERE reset_token=:1 \
|
||||
"""
|
||||
|
||||
pool = get_pool()
|
||||
|
||||
with pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, [token])
|
||||
row = cur.fetchone()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
return {"id": row[0], "expire": row[1]}
|
||||
|
||||
def set_password_service(user_id, pwd):
|
||||
hashed = generate_password_hash(pwd)
|
||||
|
||||
sql = """
|
||||
UPDATE app_users
|
||||
SET password_hash=:1,
|
||||
must_change_password=0,
|
||||
reset_token=NULL,
|
||||
reset_expire=NULL
|
||||
WHERE id=:2 \
|
||||
"""
|
||||
|
||||
pool = get_pool()
|
||||
|
||||
with pool.acquire() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, [hashed, user_id])
|
||||
conn.commit()
|
||||
|
||||
14
files/modules/users/token_service.py
Normal file
14
files/modules/users/token_service.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def generate_token():
|
||||
return secrets.token_urlsafe(48)
|
||||
|
||||
|
||||
def expiration(hours=24):
|
||||
return datetime.utcnow() + timedelta(hours=hours)
|
||||
|
||||
|
||||
def is_expired(expire_dt):
|
||||
return not expire_dt or expire_dt < datetime.utcnow()
|
||||
Reference in New Issue
Block a user