server.py 12 KB

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