server.py 13 KB

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