19 changed files with 556 additions and 30 deletions
@ -1,9 +1,15 @@ |
|||||||
FLASK_APP=run.py |
FLASK_APP=run.py |
||||||
FLASK_ENV=development |
FLASK_ENV=development |
||||||
SECRET_KEY=change-me |
SECRET_KEY=change-me |
||||||
|
APP_PORT=5090 |
||||||
|
|
||||||
MARIADB_HOST=127.0.0.1 |
MARIADB_HOST=127.0.0.1 |
||||||
MARIADB_PORT=3306 |
MARIADB_PORT=3306 |
||||||
MARIADB_DB=otb_cloud |
MARIADB_DB=otb_cloud |
||||||
MARIADB_USER=otb_cloud |
MARIADB_USER=otb_cloud |
||||||
MARIADB_PASSWORD=change-me |
MARIADB_PASSWORD=change-me |
||||||
|
|
||||||
STORAGE_ROOT=/tank/backups/otb-cloud |
STORAGE_ROOT=/tank/backups/otb-cloud |
||||||
|
|
||||||
|
OTB_PORTAL_SHARED_SECRET=change-me |
||||||
|
OTB_PORTAL_ALLOWED_SKEW_SECONDS=300 |
||||||
|
|||||||
@ -1,23 +1,21 @@ |
|||||||
import os |
|
||||||
from flask import Flask |
|
||||||
from dotenv import load_dotenv |
from dotenv import load_dotenv |
||||||
|
from flask import Flask |
||||||
|
|
||||||
|
from .core.config import Config |
||||||
|
from .db import init_app as init_db |
||||||
|
|
||||||
def create_app(): |
def create_app(): |
||||||
load_dotenv() |
load_dotenv() |
||||||
|
|
||||||
app = Flask(__name__, instance_relative_config=True) |
app = Flask(__name__, instance_relative_config=True) |
||||||
|
app.config.from_object(Config) |
||||||
|
|
||||||
app.config.from_mapping( |
init_db(app) |
||||||
SECRET_KEY=os.getenv("SECRET_KEY", "change-me"), |
|
||||||
MARIADB_HOST=os.getenv("MARIADB_HOST", "127.0.0.1"), |
|
||||||
MARIADB_PORT=int(os.getenv("MARIADB_PORT", "3306")), |
|
||||||
MARIADB_DB=os.getenv("MARIADB_DB", "otb_cloud"), |
|
||||||
MARIADB_USER=os.getenv("MARIADB_USER", "otb_cloud"), |
|
||||||
MARIADB_PASSWORD=os.getenv("MARIADB_PASSWORD", "change-me"), |
|
||||||
STORAGE_ROOT=os.getenv("STORAGE_ROOT", "/tank/backups/otb-cloud"), |
|
||||||
) |
|
||||||
|
|
||||||
from .main.routes import bp as main_bp |
from .main.routes import bp as main_bp |
||||||
|
from .auth.routes import bp as auth_bp |
||||||
|
|
||||||
app.register_blueprint(main_bp) |
app.register_blueprint(main_bp) |
||||||
|
app.register_blueprint(auth_bp) |
||||||
|
|
||||||
return app |
return app |
||||||
|
|||||||
@ -0,0 +1,59 @@ |
|||||||
|
from flask import Blueprint, current_app, redirect, render_template, request, session, url_for |
||||||
|
|
||||||
|
from app.db import get_db |
||||||
|
from .utils import ensure_user_tenant_and_devices, is_valid_signature, is_valid_timestamp |
||||||
|
|
||||||
|
bp = Blueprint("auth", __name__, url_prefix="/auth") |
||||||
|
|
||||||
|
@bp.route("/login-required") |
||||||
|
def login_required_notice(): |
||||||
|
return render_template("auth/login_required.html") |
||||||
|
|
||||||
|
@bp.route("/handoff") |
||||||
|
def handoff(): |
||||||
|
portal_user_id = request.args.get("uid", "").strip() |
||||||
|
email = request.args.get("email", "").strip().lower() |
||||||
|
ts = request.args.get("ts", "").strip() |
||||||
|
sig = request.args.get("sig", "").strip() |
||||||
|
|
||||||
|
if not portal_user_id or not email or not ts or not sig: |
||||||
|
return render_template("auth/handoff_error.html", message="Missing handoff parameters."), 400 |
||||||
|
|
||||||
|
if not is_valid_timestamp(ts): |
||||||
|
return render_template("auth/handoff_error.html", message="Handoff timestamp is invalid or expired."), 403 |
||||||
|
|
||||||
|
if not is_valid_signature(email=email, ts=ts, portal_user_id=portal_user_id, sig=sig): |
||||||
|
return render_template("auth/handoff_error.html", message="Invalid handoff signature."), 403 |
||||||
|
|
||||||
|
identity = ensure_user_tenant_and_devices(email=email, portal_user_id=int(portal_user_id)) |
||||||
|
|
||||||
|
session.clear() |
||||||
|
session["otb_user_id"] = identity["user_id"] |
||||||
|
session["otb_tenant_id"] = identity["tenant_id"] |
||||||
|
session["otb_tenant_slug"] = identity["tenant_slug"] |
||||||
|
session["otb_email"] = identity["email"] |
||||||
|
|
||||||
|
db = get_db() |
||||||
|
with db.cursor() as cur: |
||||||
|
cur.execute( |
||||||
|
""" |
||||||
|
INSERT INTO audit_logs ( |
||||||
|
tenant_id, user_id, actor_type, event_type, ip_address, user_agent, event_detail |
||||||
|
) VALUES (%s, %s, 'user', 'handoff_login_success', %s, %s, %s) |
||||||
|
""", |
||||||
|
( |
||||||
|
identity["tenant_id"], |
||||||
|
identity["user_id"], |
||||||
|
request.headers.get("X-Forwarded-For", request.remote_addr), |
||||||
|
request.headers.get("User-Agent", ""), |
||||||
|
f"Portal handoff accepted for {email}", |
||||||
|
), |
||||||
|
) |
||||||
|
db.commit() |
||||||
|
|
||||||
|
return redirect(url_for("main.dashboard")) |
||||||
|
|
||||||
|
@bp.route("/logout") |
||||||
|
def logout(): |
||||||
|
session.clear() |
||||||
|
return redirect(url_for("auth.login_required_notice")) |
||||||
@ -0,0 +1,135 @@ |
|||||||
|
import hashlib |
||||||
|
import hmac |
||||||
|
import re |
||||||
|
import time |
||||||
|
from pathlib import Path |
||||||
|
|
||||||
|
from flask import current_app |
||||||
|
|
||||||
|
from app.db import get_db |
||||||
|
|
||||||
|
SLUG_RE = re.compile(r"[^a-z0-9]+") |
||||||
|
|
||||||
|
def make_signature(email: str, ts: str, portal_user_id: str, secret: str) -> str: |
||||||
|
payload = f"{portal_user_id}|{email}|{ts}".encode("utf-8") |
||||||
|
return hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest() |
||||||
|
|
||||||
|
def is_valid_signature(email: str, ts: str, portal_user_id: str, sig: str) -> bool: |
||||||
|
secret = current_app.config["OTB_PORTAL_SHARED_SECRET"] |
||||||
|
expected = make_signature(email=email, ts=ts, portal_user_id=portal_user_id, secret=secret) |
||||||
|
return hmac.compare_digest(expected, sig) |
||||||
|
|
||||||
|
def is_valid_timestamp(ts: str) -> bool: |
||||||
|
try: |
||||||
|
ts_int = int(ts) |
||||||
|
except ValueError: |
||||||
|
return False |
||||||
|
skew = current_app.config["OTB_PORTAL_ALLOWED_SKEW_SECONDS"] |
||||||
|
return abs(int(time.time()) - ts_int) <= skew |
||||||
|
|
||||||
|
def slugify_email(email: str) -> str: |
||||||
|
local = email.split("@", 1)[0].lower().strip() |
||||||
|
slug = SLUG_RE.sub("-", local).strip("-") |
||||||
|
return slug or "tenant" |
||||||
|
|
||||||
|
def ensure_user_tenant_and_devices(email: str, portal_user_id: int): |
||||||
|
db = get_db() |
||||||
|
|
||||||
|
with db.cursor() as cur: |
||||||
|
cur.execute( |
||||||
|
""" |
||||||
|
SELECT id, portal_user_id, email, display_name |
||||||
|
FROM users |
||||||
|
WHERE email = %s |
||||||
|
""", |
||||||
|
(email,), |
||||||
|
) |
||||||
|
user = cur.fetchone() |
||||||
|
|
||||||
|
if user is None: |
||||||
|
cur.execute( |
||||||
|
""" |
||||||
|
INSERT INTO users (portal_user_id, email, display_name, is_active) |
||||||
|
VALUES (%s, %s, %s, 1) |
||||||
|
""", |
||||||
|
(portal_user_id, email, email), |
||||||
|
) |
||||||
|
user_id = cur.lastrowid |
||||||
|
else: |
||||||
|
user_id = user["id"] |
||||||
|
cur.execute( |
||||||
|
""" |
||||||
|
UPDATE users |
||||||
|
SET portal_user_id = %s, last_login_at = NOW() |
||||||
|
WHERE id = %s |
||||||
|
""", |
||||||
|
(portal_user_id, user_id), |
||||||
|
) |
||||||
|
|
||||||
|
cur.execute( |
||||||
|
""" |
||||||
|
SELECT id, slug, storage_root |
||||||
|
FROM tenants |
||||||
|
WHERE owner_user_id = %s |
||||||
|
""", |
||||||
|
(user_id,), |
||||||
|
) |
||||||
|
tenant = cur.fetchone() |
||||||
|
|
||||||
|
if tenant is None: |
||||||
|
base_slug = slugify_email(email) |
||||||
|
slug = base_slug |
||||||
|
suffix = 1 |
||||||
|
|
||||||
|
while True: |
||||||
|
cur.execute("SELECT id FROM tenants WHERE slug = %s", (slug,)) |
||||||
|
existing = cur.fetchone() |
||||||
|
if existing is None: |
||||||
|
break |
||||||
|
suffix += 1 |
||||||
|
slug = f"{base_slug}-{suffix}" |
||||||
|
|
||||||
|
storage_root = f"{current_app.config['STORAGE_ROOT']}/tenants/{slug}" |
||||||
|
cur.execute( |
||||||
|
""" |
||||||
|
INSERT INTO tenants (owner_user_id, slug, storage_root, service_status, retention_mode) |
||||||
|
VALUES (%s, %s, %s, 'active', 'standard') |
||||||
|
""", |
||||||
|
(user_id, slug, storage_root), |
||||||
|
) |
||||||
|
tenant_id = cur.lastrowid |
||||||
|
else: |
||||||
|
tenant_id = tenant["id"] |
||||||
|
slug = tenant["slug"] |
||||||
|
storage_root = tenant["storage_root"] |
||||||
|
|
||||||
|
for device_name in current_app.config["DEFAULT_DEVICE_NAMES"]: |
||||||
|
relative_path = f"devices/{device_name}" |
||||||
|
cur.execute( |
||||||
|
""" |
||||||
|
INSERT IGNORE INTO devices (tenant_id, device_name, device_type, relative_path, is_active) |
||||||
|
VALUES (%s, %s, %s, %s, 1) |
||||||
|
""", |
||||||
|
(tenant_id, device_name, device_name, relative_path), |
||||||
|
) |
||||||
|
|
||||||
|
db.commit() |
||||||
|
|
||||||
|
create_tenant_directories(slug, current_app.config["DEFAULT_DEVICE_NAMES"]) |
||||||
|
|
||||||
|
return { |
||||||
|
"user_id": user_id, |
||||||
|
"tenant_id": tenant_id, |
||||||
|
"tenant_slug": slug, |
||||||
|
"storage_root": storage_root, |
||||||
|
"email": email, |
||||||
|
} |
||||||
|
|
||||||
|
def create_tenant_directories(tenant_slug: str, device_names: list[str]): |
||||||
|
tenant_root = Path(current_app.config["STORAGE_ROOT"]) / "tenants" / tenant_slug |
||||||
|
(tenant_root / "logs").mkdir(parents=True, exist_ok=True) |
||||||
|
(tenant_root / "support").mkdir(parents=True, exist_ok=True) |
||||||
|
|
||||||
|
for device_name in device_names: |
||||||
|
for subdir in ["originals", "derived", "exports", "deleted", "tmp"]: |
||||||
|
(tenant_root / "devices" / device_name / subdir).mkdir(parents=True, exist_ok=True) |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
import os |
||||||
|
|
||||||
|
class Config: |
||||||
|
SECRET_KEY = os.getenv("SECRET_KEY", "change-me") |
||||||
|
APP_PORT = int(os.getenv("APP_PORT", "5090")) |
||||||
|
|
||||||
|
MARIADB_HOST = os.getenv("MARIADB_HOST", "127.0.0.1") |
||||||
|
MARIADB_PORT = int(os.getenv("MARIADB_PORT", "3306")) |
||||||
|
MARIADB_DB = os.getenv("MARIADB_DB", "otb_cloud") |
||||||
|
MARIADB_USER = os.getenv("MARIADB_USER", "otb_cloud") |
||||||
|
MARIADB_PASSWORD = os.getenv("MARIADB_PASSWORD", "change-me") |
||||||
|
|
||||||
|
STORAGE_ROOT = os.getenv("STORAGE_ROOT", "/tank/backups/otb-cloud") |
||||||
|
|
||||||
|
OTB_PORTAL_SHARED_SECRET = os.getenv("OTB_PORTAL_SHARED_SECRET", "change-me") |
||||||
|
OTB_PORTAL_ALLOWED_SKEW_SECONDS = int(os.getenv("OTB_PORTAL_ALLOWED_SKEW_SECONDS", "300")) |
||||||
|
|
||||||
|
DEFAULT_DEVICE_NAMES = ["laptop", "phone", "tablet", "workpc", "homepc"] |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
from flask import current_app, g |
||||||
|
import pymysql |
||||||
|
from pymysql.cursors import DictCursor |
||||||
|
|
||||||
|
def get_db(): |
||||||
|
if "db" not in g: |
||||||
|
g.db = pymysql.connect( |
||||||
|
host=current_app.config["MARIADB_HOST"], |
||||||
|
port=current_app.config["MARIADB_PORT"], |
||||||
|
user=current_app.config["MARIADB_USER"], |
||||||
|
password=current_app.config["MARIADB_PASSWORD"], |
||||||
|
database=current_app.config["MARIADB_DB"], |
||||||
|
cursorclass=DictCursor, |
||||||
|
autocommit=False, |
||||||
|
charset="utf8mb4", |
||||||
|
) |
||||||
|
return g.db |
||||||
|
|
||||||
|
def close_db(_e=None): |
||||||
|
db = g.pop("db", None) |
||||||
|
if db is not None: |
||||||
|
db.close() |
||||||
|
|
||||||
|
def init_app(app): |
||||||
|
app.teardown_appcontext(close_db) |
||||||
@ -1,7 +1,45 @@ |
|||||||
from flask import Blueprint, render_template |
from functools import wraps |
||||||
|
|
||||||
|
from flask import Blueprint, redirect, render_template, session, url_for |
||||||
|
|
||||||
|
from app.db import get_db |
||||||
|
|
||||||
bp = Blueprint("main", __name__) |
bp = Blueprint("main", __name__) |
||||||
|
|
||||||
|
def portal_session_required(view_func): |
||||||
|
@wraps(view_func) |
||||||
|
def wrapped(*args, **kwargs): |
||||||
|
if "otb_user_id" not in session or "otb_tenant_id" not in session: |
||||||
|
return redirect(url_for("auth.login_required_notice")) |
||||||
|
return view_func(*args, **kwargs) |
||||||
|
return wrapped |
||||||
|
|
||||||
@bp.route("/") |
@bp.route("/") |
||||||
def index(): |
def index(): |
||||||
return render_template("cloud/index.html") |
if "otb_user_id" in session: |
||||||
|
return redirect(url_for("main.dashboard")) |
||||||
|
return redirect(url_for("auth.login_required_notice")) |
||||||
|
|
||||||
|
@bp.route("/dashboard") |
||||||
|
@portal_session_required |
||||||
|
def dashboard(): |
||||||
|
db = get_db() |
||||||
|
|
||||||
|
with db.cursor() as cur: |
||||||
|
cur.execute( |
||||||
|
""" |
||||||
|
SELECT id, device_name, device_type, relative_path, is_active, created_at |
||||||
|
FROM devices |
||||||
|
WHERE tenant_id = %s |
||||||
|
ORDER BY id |
||||||
|
""", |
||||||
|
(session["otb_tenant_id"],), |
||||||
|
) |
||||||
|
devices = cur.fetchall() |
||||||
|
|
||||||
|
return render_template( |
||||||
|
"cloud/dashboard.html", |
||||||
|
user_email=session.get("otb_email"), |
||||||
|
tenant_slug=session.get("otb_tenant_slug"), |
||||||
|
devices=devices, |
||||||
|
) |
||||||
|
|||||||
@ -0,0 +1,10 @@ |
|||||||
|
{% extends "portal_base.html" %} |
||||||
|
|
||||||
|
{% block title %}Handoff Error{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<div class="card"> |
||||||
|
<h1>Portal handoff failed</h1> |
||||||
|
<p class="muted">{{ message }}</p> |
||||||
|
</div> |
||||||
|
{% endblock %} |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
{% extends "portal_base.html" %} |
||||||
|
|
||||||
|
{% block title %}Portal Login Required{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<div class="card"> |
||||||
|
<h1>Portal login required</h1> |
||||||
|
<p class="muted"> |
||||||
|
OTB Cloud does not allow direct unauthenticated access. |
||||||
|
This app is intended to be reached through the OTB Billing portal handoff. |
||||||
|
</p> |
||||||
|
<div class="warn" style="margin-top:16px;"> |
||||||
|
Current status: direct local scaffold only. Real portal handoff wiring is next. |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endblock %} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
{% extends "portal_base.html" %} |
||||||
|
|
||||||
|
{% block title %}OTB Cloud Dashboard{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<div class="card" style="margin-bottom:16px;"> |
||||||
|
<h1 style="margin-top:0;">OTB Cloud Dashboard</h1> |
||||||
|
<p class="muted" style="margin-bottom:10px;">Authenticated user: {{ user_email }}</p> |
||||||
|
<p class="muted" style="margin:0;">Tenant slug: <span class="badge">{{ tenant_slug }}</span></p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="grid"> |
||||||
|
<div class="card"> |
||||||
|
<h2 style="margin-top:0;">Devices</h2> |
||||||
|
{% if devices %} |
||||||
|
<ul style="padding-left:18px; margin-bottom:0;"> |
||||||
|
{% for device in devices %} |
||||||
|
<li style="margin-bottom:8px;"> |
||||||
|
<strong>{{ device.device_name }}</strong> |
||||||
|
<span class="muted">({{ device.device_type }})</span><br> |
||||||
|
<span class="muted">{{ device.relative_path }}</span> |
||||||
|
</li> |
||||||
|
{% endfor %} |
||||||
|
</ul> |
||||||
|
{% else %} |
||||||
|
<p class="muted">No devices have been created yet.</p> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="card"> |
||||||
|
<h2 style="margin-top:0;">Current scope</h2> |
||||||
|
<p class="muted"> |
||||||
|
v0.1.1 provides portal-handoff scaffolding, tenant bootstrap, device records, and an authenticated dashboard. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endblock %} |
||||||
@ -0,0 +1,112 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1"> |
||||||
|
<title>{% block title %}OTB Cloud{% endblock %}</title> |
||||||
|
<style> |
||||||
|
:root { |
||||||
|
--bg: #0b1220; |
||||||
|
--panel: #121b2d; |
||||||
|
--panel-2: #16233b; |
||||||
|
--text: #e9eef8; |
||||||
|
--muted: #9fb0cf; |
||||||
|
--line: rgba(255,255,255,0.10); |
||||||
|
--accent: #4da3ff; |
||||||
|
--danger: #ff6b6b; |
||||||
|
--ok: #4fd18b; |
||||||
|
} |
||||||
|
* { box-sizing: border-box; } |
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
font-family: Arial, sans-serif; |
||||||
|
background: linear-gradient(180deg, #08101d 0%, #0b1220 100%); |
||||||
|
color: var(--text); |
||||||
|
} |
||||||
|
a { color: var(--accent); text-decoration: none; } |
||||||
|
.wrap { max-width: 1200px; margin: 0 auto; padding: 20px; } |
||||||
|
.topbar { |
||||||
|
border-bottom: 1px solid var(--line); |
||||||
|
background: rgba(255,255,255,0.03); |
||||||
|
} |
||||||
|
.nav { |
||||||
|
display: flex; |
||||||
|
gap: 18px; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
padding: 14px 20px; |
||||||
|
max-width: 1200px; |
||||||
|
margin: 0 auto; |
||||||
|
} |
||||||
|
.brand { |
||||||
|
font-weight: bold; |
||||||
|
font-size: 20px; |
||||||
|
letter-spacing: 0.3px; |
||||||
|
} |
||||||
|
.nav-links { |
||||||
|
display: flex; |
||||||
|
gap: 14px; |
||||||
|
flex-wrap: wrap; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
.card { |
||||||
|
background: var(--panel); |
||||||
|
border: 1px solid var(--line); |
||||||
|
border-radius: 18px; |
||||||
|
padding: 20px; |
||||||
|
box-shadow: 0 10px 24px rgba(0,0,0,0.18); |
||||||
|
} |
||||||
|
.grid { |
||||||
|
display: grid; |
||||||
|
gap: 16px; |
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); |
||||||
|
} |
||||||
|
.muted { color: var(--muted); } |
||||||
|
.badge { |
||||||
|
display: inline-block; |
||||||
|
padding: 6px 10px; |
||||||
|
border-radius: 999px; |
||||||
|
background: rgba(77,163,255,0.12); |
||||||
|
border: 1px solid rgba(77,163,255,0.22); |
||||||
|
color: var(--text); |
||||||
|
font-size: 12px; |
||||||
|
} |
||||||
|
.footer { |
||||||
|
border-top: 1px solid var(--line); |
||||||
|
margin-top: 28px; |
||||||
|
color: var(--muted); |
||||||
|
} |
||||||
|
.footer .wrap { |
||||||
|
padding-top: 16px; |
||||||
|
padding-bottom: 24px; |
||||||
|
} |
||||||
|
.warn { |
||||||
|
border-left: 4px solid var(--danger); |
||||||
|
background: rgba(255,107,107,0.08); |
||||||
|
padding: 14px 16px; |
||||||
|
border-radius: 12px; |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div class="topbar"> |
||||||
|
<div class="nav"> |
||||||
|
<div class="brand">OTB Cloud</div> |
||||||
|
<div class="nav-links"> |
||||||
|
<a href="/dashboard">Dashboard</a> |
||||||
|
<a href="/auth/logout">Logout</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="wrap"> |
||||||
|
{% block content %}{% endblock %} |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="footer"> |
||||||
|
<div class="wrap"> |
||||||
|
Temporary local portal base template for OTB Cloud v0.1.1. Replace this with the shared OTB portal base during integration. |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -1,3 +1,4 @@ |
|||||||
Flask==3.1.0 |
Flask==3.1.0 |
||||||
PyMySQL==1.1.1 |
PyMySQL==1.1.1 |
||||||
python-dotenv==1.0.1 |
python-dotenv==1.0.1 |
||||||
|
cryptography==44.0.2 |
||||||
|
|||||||
@ -0,0 +1,21 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
set -e |
||||||
|
|
||||||
|
cd /opt/otb_cloud || exit 1 |
||||||
|
|
||||||
|
echo "===== creating MariaDB database and user =====" |
||||||
|
echo "You will be prompted for sudo password because this uses sudo mysql." |
||||||
|
|
||||||
|
sudo mysql <<'SQL' |
||||||
|
CREATE DATABASE IF NOT EXISTS otb_cloud CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
||||||
|
CREATE USER IF NOT EXISTS 'otb_cloud'@'localhost' IDENTIFIED BY 'change-me'; |
||||||
|
GRANT ALL PRIVILEGES ON otb_cloud.* TO 'otb_cloud'@'localhost'; |
||||||
|
FLUSH PRIVILEGES; |
||||||
|
SQL |
||||||
|
|
||||||
|
echo "===== importing schema =====" |
||||||
|
mysql -uotb_cloud -pchange-me otb_cloud < app/models/schema.sql |
||||||
|
|
||||||
|
echo "===== database bootstrap completed =====" |
||||||
|
echo "===== IMPORTANT: change the MariaDB password in both MariaDB and .env before production =====" |
||||||
|
echo "===== script completed successfully =====" |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
import hashlib |
||||||
|
import hmac |
||||||
|
import os |
||||||
|
import time |
||||||
|
import urllib.parse |
||||||
|
|
||||||
|
secret = os.getenv("OTB_PORTAL_SHARED_SECRET", "change-me") |
||||||
|
uid = os.getenv("OTB_TEST_UID", "1001") |
||||||
|
email = os.getenv("OTB_TEST_EMAIL", "client@example.com") |
||||||
|
ts = str(int(time.time())) |
||||||
|
|
||||||
|
payload = f"{uid}|{email}|{ts}".encode("utf-8") |
||||||
|
sig = hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest() |
||||||
|
|
||||||
|
params = urllib.parse.urlencode({ |
||||||
|
"uid": uid, |
||||||
|
"email": email, |
||||||
|
"ts": ts, |
||||||
|
"sig": sig, |
||||||
|
}) |
||||||
|
|
||||||
|
print(f"http://127.0.0.1:5090/auth/handoff?{params}") |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
set -e |
||||||
|
|
||||||
|
cd /opt/otb_cloud || exit 1 |
||||||
|
|
||||||
|
python3 -m venv venv |
||||||
|
source venv/bin/activate |
||||||
|
pip install --upgrade pip |
||||||
|
pip install -r requirements.txt |
||||||
|
|
||||||
|
echo "===== venv ready =====" |
||||||
|
echo "===== activate with: source /opt/otb_cloud/venv/bin/activate =====" |
||||||
|
echo "===== script completed successfully =====" |
||||||
Loading…
Reference in new issue