server.py 12 KB

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