server.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. import os
  2. import ssl
  3. import arrow
  4. import flask_profiler
  5. import sentry_sdk
  6. from flask import Flask, redirect, url_for, render_template, request, jsonify
  7. from flask_admin import Admin
  8. from flask_cors import cross_origin
  9. from flask_login import current_user
  10. from sentry_sdk.integrations.flask import FlaskIntegration
  11. from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
  12. from sentry_sdk.integrations.aiohttp import AioHttpIntegration
  13. from app import paddle_utils
  14. from app.admin_model import SLModelView, SLAdminIndexView
  15. from app.api.base import api_bp
  16. from app.auth.base import auth_bp
  17. from app.config import (
  18. DB_URI,
  19. FLASK_SECRET,
  20. SENTRY_DSN,
  21. URL,
  22. SHA1,
  23. PADDLE_MONTHLY_PRODUCT_ID,
  24. RESET_DB,
  25. FLASK_PROFILER_PATH,
  26. FLASK_PROFILER_PASSWORD,
  27. )
  28. from app.dashboard.base import dashboard_bp
  29. from app.developer.base import developer_bp
  30. from app.discover.base import discover_bp
  31. from app.extensions import db, login_manager, migrate
  32. from app.jose_utils import get_jwk_key
  33. from app.log import LOG
  34. from app.models import (
  35. Client,
  36. User,
  37. ClientUser,
  38. GenEmail,
  39. RedirectUri,
  40. Subscription,
  41. PlanEnum,
  42. ApiKey,
  43. CustomDomain,
  44. LifetimeCoupon,
  45. )
  46. from app.monitor.base import monitor_bp
  47. from app.oauth.base import oauth_bp
  48. if SENTRY_DSN:
  49. LOG.d("enable sentry")
  50. sentry_sdk.init(
  51. dsn=SENTRY_DSN,
  52. integrations=[
  53. FlaskIntegration(),
  54. SqlalchemyIntegration(),
  55. AioHttpIntegration(),
  56. ],
  57. )
  58. # the app is served behin nginx which uses http and not https
  59. os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
  60. def create_app() -> Flask:
  61. app = Flask(__name__)
  62. app.url_map.strict_slashes = False
  63. app.config["SQLALCHEMY_DATABASE_URI"] = DB_URI
  64. app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
  65. app.secret_key = FLASK_SECRET
  66. app.config["TEMPLATES_AUTO_RELOAD"] = True
  67. # to avoid conflict with other cookie
  68. app.config["SESSION_COOKIE_NAME"] = "slapp"
  69. init_extensions(app)
  70. register_blueprints(app)
  71. set_index_page(app)
  72. jinja2_filter(app)
  73. setup_error_page(app)
  74. setup_favicon_route(app)
  75. setup_openid_metadata(app)
  76. init_admin(app)
  77. setup_paddle_callback(app)
  78. setup_do_not_track(app)
  79. if FLASK_PROFILER_PATH:
  80. LOG.d("Enable flask-profiler")
  81. app.config["flask_profiler"] = {
  82. "enabled": True,
  83. "storage": {"engine": "sqlite", "FILE": FLASK_PROFILER_PATH},
  84. "basicAuth": {
  85. "enabled": True,
  86. "username": "admin",
  87. "password": FLASK_PROFILER_PASSWORD,
  88. },
  89. "ignore": ["^/static/.*", "/git", "/exception"],
  90. }
  91. flask_profiler.init_app(app)
  92. return app
  93. def fake_data():
  94. LOG.d("create fake data")
  95. # Remove db if exist
  96. if os.path.exists("db.sqlite"):
  97. LOG.d("remove existing db file")
  98. os.remove("db.sqlite")
  99. # Create all tables
  100. db.create_all()
  101. # Create a user
  102. user = User.create(
  103. email="john@wick.com",
  104. name="John Wick",
  105. password="password",
  106. activated=True,
  107. is_admin=True,
  108. otp_secret="base32secret3232",
  109. )
  110. db.session.commit()
  111. LifetimeCoupon.create(code="coupon", nb_used=10)
  112. db.session.commit()
  113. # Create a subscription for user
  114. Subscription.create(
  115. user_id=user.id,
  116. cancel_url="https://checkout.paddle.com/subscription/cancel?user=1234",
  117. update_url="https://checkout.paddle.com/subscription/update?user=1234",
  118. subscription_id="123",
  119. event_time=arrow.now(),
  120. next_bill_date=arrow.now().shift(days=10).date(),
  121. plan=PlanEnum.monthly,
  122. )
  123. db.session.commit()
  124. api_key = ApiKey.create(user_id=user.id, name="Chrome")
  125. api_key.code = "code"
  126. GenEmail.create_new(user.id, "e1@")
  127. GenEmail.create_new(user.id, "e2@")
  128. GenEmail.create_new(user.id, "e3@")
  129. CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True)
  130. CustomDomain.create(
  131. user_id=user.id, domain="very-long-domain.com.net.org", verified=True
  132. )
  133. db.session.commit()
  134. # Create a client
  135. client1 = Client.create_new(name="Demo", user_id=user.id)
  136. client1.oauth_client_id = "client-id"
  137. client1.oauth_client_secret = "client-secret"
  138. client1.published = True
  139. db.session.commit()
  140. RedirectUri.create(client_id=client1.id, uri="https://ab.com")
  141. client2 = Client.create_new(name="Demo 2", user_id=user.id)
  142. client2.oauth_client_id = "client-id2"
  143. client2.oauth_client_secret = "client-secret2"
  144. client2.published = True
  145. db.session.commit()
  146. db.session.commit()
  147. @login_manager.user_loader
  148. def load_user(user_id):
  149. user = User.query.get(user_id)
  150. return user
  151. def register_blueprints(app: Flask):
  152. app.register_blueprint(auth_bp)
  153. app.register_blueprint(monitor_bp)
  154. app.register_blueprint(dashboard_bp)
  155. app.register_blueprint(developer_bp)
  156. app.register_blueprint(oauth_bp, url_prefix="/oauth")
  157. app.register_blueprint(oauth_bp, url_prefix="/oauth2")
  158. app.register_blueprint(discover_bp)
  159. app.register_blueprint(api_bp)
  160. def set_index_page(app):
  161. @app.route("/", methods=["GET", "POST"])
  162. def index():
  163. if current_user.is_authenticated:
  164. return redirect(url_for("dashboard.index"))
  165. else:
  166. return redirect(url_for("auth.login"))
  167. @app.after_request
  168. def after_request(res):
  169. # not logging /static call
  170. if (
  171. not request.path.startswith("/static")
  172. and not request.path.startswith("/admin/static")
  173. and not request.path.startswith("/_debug_toolbar")
  174. ):
  175. LOG.debug(
  176. "%s %s %s %s %s",
  177. request.remote_addr,
  178. request.method,
  179. request.path,
  180. request.args,
  181. res.status_code,
  182. )
  183. res.headers["X-Frame-Options"] = "deny"
  184. return res
  185. def setup_openid_metadata(app):
  186. @app.route("/.well-known/openid-configuration")
  187. @cross_origin()
  188. def openid_config():
  189. res = {
  190. "issuer": URL,
  191. "authorization_endpoint": URL + "/oauth2/authorize",
  192. "token_endpoint": URL + "/oauth2/token",
  193. "userinfo_endpoint": URL + "/oauth2/userinfo",
  194. "jwks_uri": URL + "/jwks",
  195. "response_types_supported": [
  196. "code",
  197. "token",
  198. "id_token",
  199. "id_token token",
  200. "id_token code",
  201. ],
  202. "subject_types_supported": ["public"],
  203. "id_token_signing_alg_values_supported": ["RS256"],
  204. # todo: add introspection and revocation endpoints
  205. # "introspection_endpoint": URL + "/oauth2/token/introspection",
  206. # "revocation_endpoint": URL + "/oauth2/token/revocation",
  207. }
  208. return jsonify(res)
  209. @app.route("/jwks")
  210. @cross_origin()
  211. def jwks():
  212. res = {"keys": [get_jwk_key()]}
  213. return jsonify(res)
  214. def setup_error_page(app):
  215. @app.errorhandler(400)
  216. def page_not_found(e):
  217. return render_template("error/400.html"), 400
  218. @app.errorhandler(401)
  219. def page_not_found(e):
  220. return render_template("error/401.html", current_url=request.full_path), 401
  221. @app.errorhandler(403)
  222. def page_not_found(e):
  223. return render_template("error/403.html"), 403
  224. @app.errorhandler(404)
  225. def page_not_found(e):
  226. return render_template("error/404.html"), 404
  227. @app.errorhandler(Exception)
  228. def error_handler(e):
  229. LOG.exception(e)
  230. return render_template("error/500.html"), 500
  231. def setup_favicon_route(app):
  232. @app.route("/favicon.ico")
  233. def favicon():
  234. return redirect("/static/favicon.ico")
  235. def jinja2_filter(app):
  236. def format_datetime(value):
  237. dt = arrow.get(value)
  238. return dt.humanize()
  239. app.jinja_env.filters["dt"] = format_datetime
  240. @app.context_processor
  241. def inject_stage_and_region():
  242. return dict(YEAR=arrow.now().year, URL=URL, SENTRY_DSN=SENTRY_DSN, VERSION=SHA1)
  243. def setup_paddle_callback(app: Flask):
  244. @app.route("/paddle", methods=["GET", "POST"])
  245. def paddle():
  246. LOG.debug(
  247. "paddle callback %s %s %s %s %s",
  248. request.form.get("alert_name"),
  249. request.form.get("email"),
  250. request.form.get("customer_name"),
  251. request.form.get("subscription_id"),
  252. request.form.get("subscription_plan_id"),
  253. )
  254. # make sure the request comes from Paddle
  255. if not paddle_utils.verify_incoming_request(dict(request.form)):
  256. LOG.error(
  257. "request not coming from paddle. Request data:%s", dict(request.form)
  258. )
  259. return "KO", 400
  260. if (
  261. request.form.get("alert_name") == "subscription_created"
  262. ): # new user subscribes
  263. user_email = request.form.get("email")
  264. user = User.get_by(email=user_email)
  265. if (
  266. int(request.form.get("subscription_plan_id"))
  267. == PADDLE_MONTHLY_PRODUCT_ID
  268. ):
  269. plan = PlanEnum.monthly
  270. else:
  271. plan = PlanEnum.yearly
  272. sub = Subscription.get_by(user_id=user.id)
  273. if not sub:
  274. LOG.d("create a new sub")
  275. Subscription.create(
  276. user_id=user.id,
  277. cancel_url=request.form.get("cancel_url"),
  278. update_url=request.form.get("update_url"),
  279. subscription_id=request.form.get("subscription_id"),
  280. event_time=arrow.now(),
  281. next_bill_date=arrow.get(
  282. request.form.get("next_bill_date"), "YYYY-MM-DD"
  283. ).date(),
  284. plan=plan,
  285. )
  286. else:
  287. LOG.d("update existing sub %s", sub)
  288. sub.cancel_url = request.form.get("cancel_url")
  289. sub.update_url = request.form.get("update_url")
  290. sub.subscription_id = request.form.get("subscription_id")
  291. sub.event_time = arrow.now()
  292. sub.next_bill_date = arrow.get(
  293. request.form.get("next_bill_date"), "YYYY-MM-DD"
  294. ).date()
  295. sub.plan = plan
  296. LOG.debug("User %s upgrades!", user)
  297. db.session.commit()
  298. elif request.form.get("alert_name") == "subscription_updated":
  299. subscription_id = request.form.get("subscription_id")
  300. LOG.debug("Update subscription %s", subscription_id)
  301. sub: Subscription = Subscription.get_by(subscription_id=subscription_id)
  302. sub.event_time = arrow.now()
  303. sub.next_bill_date = arrow.get(
  304. request.form.get("next_bill_date"), "YYYY-MM-DD"
  305. ).date()
  306. db.session.commit()
  307. elif request.form.get("alert_name") == "subscription_cancelled":
  308. subscription_id = request.form.get("subscription_id")
  309. LOG.debug("Cancel subscription %s", subscription_id)
  310. sub: Subscription = Subscription.get_by(subscription_id=subscription_id)
  311. if sub:
  312. sub.cancelled = True
  313. db.session.commit()
  314. return "OK"
  315. def init_extensions(app: Flask):
  316. LOG.debug("init extensions")
  317. login_manager.init_app(app)
  318. db.init_app(app)
  319. migrate.init_app(app)
  320. def init_admin(app):
  321. admin = Admin(name="SimpleLogin", template_mode="bootstrap3")
  322. admin.init_app(app, index_view=SLAdminIndexView())
  323. admin.add_view(SLModelView(User, db.session))
  324. admin.add_view(SLModelView(Client, db.session))
  325. admin.add_view(SLModelView(GenEmail, db.session))
  326. admin.add_view(SLModelView(ClientUser, db.session))
  327. def setup_do_not_track(app):
  328. @app.route("/dnt")
  329. def do_not_track():
  330. return """
  331. <script>
  332. // Disable GoatCounter if this script is called
  333. window.localStorage.setItem('goatcounter-ignore', 't');
  334. alert("GoatCounter disabled");
  335. window.location.href = "/";
  336. </script>
  337. """
  338. if __name__ == "__main__":
  339. app = create_app()
  340. app.debug = True
  341. # enable flask toolbar
  342. # app.config["DEBUG_TB_PROFILER_ENABLED"] = True
  343. # app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = False
  344. #
  345. # toolbar = DebugToolbarExtension(app)
  346. # enable to print all queries generated by sqlalchemy
  347. # app.config["SQLALCHEMY_ECHO"] = True
  348. # warning: only used in local
  349. if RESET_DB:
  350. LOG.warning("reset db, add fake data")
  351. with app.app_context():
  352. fake_data()
  353. if URL.startswith("https"):
  354. LOG.d("enable https")
  355. context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
  356. context.load_cert_chain("local_data/cert.pem", "local_data/key.pem")
  357. app.run(debug=True, host="0.0.0.0", port=7777, ssl_context=context)
  358. else:
  359. app.run(debug=True, host="0.0.0.0", port=7777)