server.py 14 KB

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