diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..b27712dd --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +db.sqlite \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1945b032..14c437c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .idea/ *.pyc -db.sqlite \ No newline at end of file +db.sqlite +.env +.pytest_cache +.vscode \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..89483464 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.7 + +RUN apt-get update + +WORKDIR /code + +COPY ./requirements.txt ./ +RUN pip3 install -r requirements.txt + + +COPY . . + +CMD gunicorn wsgi:app -b 0.0.0.0:5000 -w 2 --timeout 15 --log-level DEBUG + +#CMD ["/usr/local/bin/gunicorn", "wsgi:app", "-k", "gthread", "-b", "0.0.0.0:5000", "-w", "2", "--timeout", "15", "--log-level", "DEBUG"] diff --git a/README.md b/README.md new file mode 100644 index 00000000..c5e7f478 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ + +## OAuth flow + +Authorization code flow: + +http://sl-server:5000/oauth/authorize?client_id=client-id&state=123456&response_type=code&redirect_uri=http%3A%2F%2Fsl-client%3A7000%2Fcallback&state=dvoQ6Jtv0PV68tBUgUMM035oFiZw57 + +Implicit flow: +http://sl-server:5000/oauth/authorize?client_id=client-id&state=123456&response_type=token&redirect_uri=http%3A%2F%2Fsl-client%3A7000%2Fcallback&state=dvoQ6Jtv0PV68tBUgUMM035oFiZw57 + +Exchange the code to get the token with `{code}` replaced by the code obtained in previous step. + +http -f -a client-id:client-secret http://localhost:5000/oauth/token grant_type=authorization_code code={code} + +Get user info: + +http http://localhost:5000/oauth/user_info 'Authorization:Bearer {token}' + + +## Template structure + +base + single: for login, register page + default: for all pages when user log ins + +## How to create new migration + +Whenever the model changes, a new migration needs to be created + +Set the database connection to use staging environment: + +> set -x CONFIG ~/config/simplelogin/staging.env + +Generate the migration script and make sure to review it: + +> flask db migrate + +## Code structure + +local_data/: contain files used only locally. In deployment, these files should be replaced. + - jwtRS256.key: generated using + +```bash +ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key +# Don't add passphrase +openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub +``` + +## OpenID, OAuth2 response_type & scope + +According to https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660 + +- `response_type` can be either `code, token, id_token` or any combination. +- `scope` can contain `openid` or not + +Below is the different combinations that are taken into account until now: + +response_type=code + scope: + with `openid` in scope, return `id_token` at /token: OK + without: OK + +response_type=token + scope: + with and without `openid`, nothing to do: OK + +response_type=id_token + return `id_token` in /authorization endpoint + +response_type=id_token token + return `id_token` in addition to `access_token` in /authorization endpoint + +response_type=id_token code + return `id_token` in addition to `authorization_code` in /authorization endpoint + + +# Plan Upgrade, downgrade flow + +Here's an example: + +July 2019: user takes yearly plan, valid until July 2020 + user.plan=yearly, user.plan_expiration=None + set user.stripe card-token, customer-id, subscription-id + +December 2019: user cancels his plan. + set plan_expiration to "period end of subscription", ie July 2020 + call stripe: + stripe.Subscription.modify( + user.stripe_subscription_id, + cancel_at_period_end=True + ) + +There are 2 possible scenarios at this point: +1) user decides to renew on March 2020: + set plan_expiration = None + stripe.Subscription.modify( + user.stripe_subscription_id, + cancel_at_period_end=False + ) + +2) the plan ends on July 2020. +The cronjob set +- user stripe_subscription_id , stripe_card_token, stripe_customer_id to None +- user.plan=free, user.plan_expiration=None +- delete customer on stripe + +user decides to take the premium plan again: go through all normal flow + + + diff --git a/app/admin_model.py b/app/admin_model.py new file mode 100644 index 00000000..f4f436c6 --- /dev/null +++ b/app/admin_model.py @@ -0,0 +1,22 @@ +from flask import redirect, url_for, request +from flask_admin import expose, AdminIndexView +from flask_admin.contrib import sqla +from flask_login import current_user + + +class SLModelView(sqla.ModelView): + def is_accessible(self): + return current_user.is_authenticated and current_user.is_admin + + def inaccessible_callback(self, name, **kwargs): + # redirect to login page if user doesn't have access + return redirect(url_for("auth.login", next=request.url)) + + +class SLAdminIndexView(AdminIndexView): + @expose("/") + def index(self): + if not current_user.is_authenticated or not current_user.is_admin: + return redirect(url_for("auth.login", next=request.url)) + + return super(SLAdminIndexView, self).index() diff --git a/app/auth/__init__.py b/app/auth/__init__.py index a4a4b69c..4c496853 100644 --- a/app/auth/__init__.py +++ b/app/auth/__init__.py @@ -1 +1,9 @@ -from .views import login, logout +from .views import ( + login, + logout, + register, + activate, + resend_activation, + reset_password, + forgot_password, +) diff --git a/app/auth/base.py b/app/auth/base.py index 72638fee..5418e7c5 100644 --- a/app/auth/base.py +++ b/app/auth/base.py @@ -1,3 +1,5 @@ from flask import Blueprint -auth_bp = Blueprint(name="auth", import_name=__name__, url_prefix="/auth") +auth_bp = Blueprint( + name="auth", import_name=__name__, url_prefix="/auth", template_folder="templates" +) diff --git a/app/auth/templates/auth/activate.html b/app/auth/templates/auth/activate.html new file mode 100644 index 00000000..0dd11e0b --- /dev/null +++ b/app/auth/templates/auth/activate.html @@ -0,0 +1,16 @@ +{% extends "error.html" %} + +{% block error_name %} + {{ error }} +{% endblock %} + +{% block error_description %} + + {% if show_resend_activation %} +
+ Used On + + | +Actions | ++ Enable/Disable Email Forwarding + | +Created At | +|
---|---|---|---|---|
+ + | + +
+ {% for client_user in gen_email.client_users %}
+ {{ client_user.client.name }} + {% endfor %} + |
+
+
+
+
+
+
+
+ |
+
+ + + | + ++ {{ gen_email.created_at | dt }} + | +
+ App + | ++ Information + + | ++ First used + + | + +
---|---|---|
+ {{ client_user.client.name }} + | + +
+ {% for scope, val in client_user.get_user_info().items() %}
+
+ {% if scope == "email" %}
+ Email: {{ val }}
+ {% elif scope == "name" %}
+ Name: {{ val }}
+ {% endif %}
+
+ {% endfor %}
+ |
+
+
+ + {{ client_user.created_at | dt }} + | + + {# TODO: add last_used#} + + +
+ | Name | +OAuth2 Client ID | +Scopes | +Number Users | +Edit | + +Delete | +
---|---|---|---|---|---|---|
+ {% if client.icon_id %}
+
+
+
+ {% endif %}
+ |
+
+
+
+
+ Created at: {{ client.created_at |dt }}
+
+ |
+
+ + {{ client.oauth_client_id }} + | + +
+
|
+
+ + {{ client.nb_user() }} + | + ++ + + + | + + + ++ + | +
+ App/Website that have implemented Connect with SimpeLogin +
+ +