Skip to content

Commit

Permalink
Merge pull request #1 from silentworks/develop
Browse files Browse the repository at this point in the history
Latest update with some fixes to supabase-py
  • Loading branch information
silentworks committed Sep 29, 2023
2 parents 091872f + 5bd80ae commit 8b4ab10
Show file tree
Hide file tree
Showing 33 changed files with 4,115 additions and 466 deletions.
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
}
},
"deno.enablePaths": [
"supabase"
]
}
31 changes: 26 additions & 5 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from flask import Flask, render_template
from app.utils import (
login_required,
password_update_required,
from flask import Flask, abort, render_template
from app.supabase import (
session_context_processor,
get_profile_by_slug,
get_profile_by_user,
)
from app.decorators import login_required, password_update_required, profile_required
from app.auth import auth
from app.account import account
from app.notes import notes

app = Flask(__name__, template_folder="../templates", static_folder="../static")

Expand All @@ -14,10 +16,29 @@
app.context_processor(session_context_processor)
app.register_blueprint(auth)
app.register_blueprint(account)
app.register_blueprint(notes)


@app.route("/")
@login_required
@password_update_required
@profile_required
def home():
return render_template("index.html")
profile = get_profile_by_user()
return render_template("index.html", profile=profile)


@app.route("/u/<slug>")
def u(slug):
profile = get_profile_by_slug(slug)
return render_template("profile.html", profile=profile)


@app.route("/service-unavailable")
def service_unavailable():
return render_template("5xx.html", title="Service Unavailable!")


@app.errorhandler(404)
def page_not_found(e):
return render_template("404.html", title="Page Not Found!"), 404
68 changes: 55 additions & 13 deletions app/account.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
from flask import Blueprint, render_template, flash, session
from flask import Blueprint, render_template, flash, request, session
from gotrue.errors import AuthApiError
from postgrest.exceptions import APIError

from app.forms import UpdateEmailForm, UpdatePasswordForm
from app.utils import (
login_required,
password_update_required,
session_context_processor,
supabase,
)
from app.forms import UpdateEmailForm, UpdateForm, UpdatePasswordForm
from app.supabase import get_profile_by_user, session_context_processor, supabase
from app.decorators import login_required, password_update_required, profile_required

account = Blueprint("account", __name__, url_prefix="/account")
account.context_processor(session_context_processor)
Expand All @@ -16,14 +13,57 @@
@account.route("/")
@login_required
@password_update_required
@profile_required
def home():
return render_template("account/index.html")
profile = get_profile_by_user()
return render_template("account/index.html", profile=profile)


@account.route("/update", methods=["GET", "POST"])
@login_required
def update():
profile = get_profile_by_user()
form = UpdateForm(data=profile)
if form.validate_on_submit():
display_name = form.display_name.data
bio = form.bio.data
first_name = form.first_name.data
last_name = form.last_name.data
dob = form.dob.data
profile_location = form.profile_location.data

try:
res = supabase.rpc(
fn="update_profile",
params={
"display_name": display_name,
"bio": bio,
"first_name": first_name,
"last_name": last_name,
"dob": dob,
"profile_location": profile_location,
},
).execute()

if res:
flash("Your profile was updated successfully.", "info")
else:
flash(
"Updating your profile failed, please try again.",
"error",
)
except APIError as exception:
flash(exception.message, "error")

return render_template("account/update.html", form=form, profile=profile)


@account.route("/update-email", methods=["GET", "POST"])
@login_required
@password_update_required
@profile_required
def update_email():
profile = get_profile_by_user()
form = UpdateEmailForm()
if form.validate_on_submit():
email = form.email.data
Expand All @@ -32,22 +72,24 @@ def update_email():
user = supabase.auth.update_user(attributes={"email": email})

if user:
flash("Your email was updated successfully.", category="info")
flash("Your email was updated successfully.", "info")
else:
flash(
"Updating your email address failed, please try again.",
category="error",
"error",
)
except AuthApiError as exception:
err = exception.to_dict()
flash(err.get("message"), "error")

return render_template("account/update-email.html", form=form)
return render_template("account/update-email.html", form=form, profile=profile)


@account.route("/update-password", methods=["GET", "POST"])
@login_required
@profile_required
def update_password():
profile = get_profile_by_user()
form = UpdatePasswordForm()
if form.validate_on_submit():
password = form.password.data
Expand All @@ -64,4 +106,4 @@ def update_password():
err = exception.to_dict()
flash(err.get("message"), "error")

return render_template("account/update-password.html", form=form)
return render_template("account/update-password.html", form=form, profile=profile)
40 changes: 38 additions & 2 deletions app/auth.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import os
from flask import Blueprint, render_template, redirect, request, session, url_for, flash
from app.forms import AuthForm, ForgotPasswordForm
from app.utils import supabase
from app.forms import AuthForm, ForgotPasswordForm, VerifyTokenForm
from app.supabase import supabase
from gotrue.errors import AuthApiError

auth = Blueprint("auth", __name__, url_prefix="/auth")
supabase_key = os.environ.get("SUPABASE_KEY", "")


@auth.route("/signin", methods=["GET", "POST"])
Expand Down Expand Up @@ -47,6 +49,9 @@ def signup():
@auth.route("/signout", methods=["POST"])
def signout():
supabase.auth.sign_out()
# TODO: remove workaround once
# https://github.com/supabase-community/supabase-py/pull/560 is merged and released
# supabase.postgrest.auth(token=supabase_key)
return redirect(url_for("auth.signin"))


Expand Down Expand Up @@ -78,3 +83,34 @@ def confirm():
supabase.auth.verify_otp(params={"token_hash": token_hash, "type": auth_type})

return redirect(url_for(next))


@auth.route("/verify-token", methods=["GET", "POST"])
def verify_token():
auth_type = request.args.get("type", "email")
next = request.args.get("next", "home")
form = VerifyTokenForm()
if form.validate_on_submit():
email = form.email.data
token = form.token.data

if auth_type:
if auth_type == "recovery":
session["password_update_required"] = True

try:
supabase.auth.verify_otp(
params={"email": email, "token": token, "type": auth_type}
)
return redirect(url_for(next))
except AuthApiError as exception:
err = exception.to_dict()
message = err.get("message")
if err.get("message") == "User not found":
message = "Email provided is not recognised"

flash(message, "error")

return render_template(
"auth/verify-token.html", form=form, next=next, auth_type=auth_type
)
64 changes: 64 additions & 0 deletions app/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from functools import wraps
from typing import Union
from flask import redirect, session, url_for, request
from gotrue.errors import AuthApiError, AuthRetryableError
from gotrue.types import Session
from app.supabase import get_profile_by_user, supabase


def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
sess: Union[Session, None] = None
try:
sess = supabase.auth.get_session()
# TODO: remove workaround once
# https://github.com/supabase-community/supabase-py/pull/560 is merged and released
# supabase.postgrest.auth(token=sess.access_token)
except AuthApiError as exception:
err = exception.to_dict()
if err.get("message") == "Invalid Refresh Token: Already Used":
sess = None
except AuthRetryableError:
return redirect(url_for("service_unavailable"))

if sess is None:
return redirect(url_for("auth.signin", next=request.endpoint))

return f(*args, **kwargs)

return decorated


def password_update_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if "password_update_required" in session:
return redirect(url_for("account.update_password"))

return f(*args, **kwargs)

return decorated


def profile_required(f):
@wraps(f)
def decorated(*args, **kwargs):
incomplete_profile = False
try:
# get profile and profile_info
profile = get_profile_by_user()

if profile and profile["display_name"] is None:
incomplete_profile = True
except AuthApiError:
incomplete_profile = True
except AuthRetryableError:
incomplete_profile = True

if incomplete_profile:
return redirect(url_for("account.update"))

return f(*args, **kwargs)

return decorated
47 changes: 44 additions & 3 deletions app/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from flask_wtf import FlaskForm
from wtforms import PasswordField, EmailField
from wtforms.validators import Email, InputRequired, Length, EqualTo
from wtforms.widgets import PasswordInput
from wtforms import (
PasswordField,
EmailField,
StringField,
TextAreaField,
BooleanField,
FileField,
)
from wtforms.validators import Email, InputRequired, Length, EqualTo, Optional


class AuthForm(FlaskForm):
Expand All @@ -19,6 +25,32 @@ class ForgotPasswordForm(FlaskForm):
)


class VerifyTokenForm(FlaskForm):
email = EmailField(
"Email", validators=[InputRequired("Email is required."), Email()]
)
token = StringField("Token", validators=[InputRequired("Token is required.")])


class UpdateForm(FlaskForm):
bio = TextAreaField("Bio", validators=[Optional()])
display_name = StringField(
"Display name", validators=[InputRequired("Display name is required.")]
)
first_name = StringField(
"First name", validators=[InputRequired("First name is required.")]
)
last_name = StringField(
"Last name", validators=[InputRequired("Last name is required.")]
)
dob = StringField(
"Date of birth", validators=[InputRequired("Date of birth is required.")]
)
profile_location = StringField(
"Location", validators=[InputRequired("Location is required.")]
)


class UpdateEmailForm(FlaskForm):
email = EmailField(
"Email",
Expand All @@ -44,3 +76,12 @@ class UpdatePasswordForm(FlaskForm):
EqualTo(fieldname="password", message="Password does not match"),
],
)


class NoteForm(FlaskForm):
title = StringField("Title", validators=[InputRequired("Title is required.")])
content = TextAreaField(
"Content", validators=[InputRequired("Content is required.")]
)
featured_image = FileField("Featured Image", validators=[Optional()])
is_public = BooleanField("Is public", validators=[Optional()])
Loading

0 comments on commit 8b4ab10

Please sign in to comment.