Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from flask import Flask, abort, g, render_template
from dotenv import load_dotenv

from flask_misaka import Misaka
load_dotenv() # take environment variables from .env.
from flaskext.markdown import Markdown
from app.supabase import (
supabase,
user_context_processor,
Expand All @@ -18,7 +20,7 @@

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

Misaka(app)
Markdown(app)

# Set the secret key to some random bytes. Keep this really secret!
app.secret_key = b"c8af64a6a0672678800db3c5a3a8d179f386e083f559518f2528202a4b7de8f8"
Expand Down
73 changes: 69 additions & 4 deletions app/account.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from flask import Blueprint, redirect, render_template, flash, request, session, url_for
from flask_wtf import FlaskForm
from gotrue.errors import AuthApiError
from supabase import AuthApiError, FunctionsRelayError, FunctionsHttpError
from postgrest.exceptions import APIError
from supafunc.errors import FunctionsRelayError, FunctionsHttpError

from app.forms import UpdateEmailForm, UpdateForm, UpdatePasswordForm
from app.supabase import get_profile_by_user, user_context_processor, supabase
Expand Down Expand Up @@ -110,10 +109,76 @@ def update_password():

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

@account.route("/connect", methods=["GET", "POST"])
@login_required
@profile_required
def list_identities():
profile = get_profile_by_user()
res = supabase.auth.get_user_identities()
identities = res.identities
connected_identities = list(
map(lambda identity: identity.provider, res.identities)
)
form = UpdatePasswordForm()
if form.validate_on_submit():
password = form.password.data

try:
user = supabase.auth.update_user(attributes={"password": password})

if user:
flash("Your password was updated successfully.", "info")
session.pop("password_update_required", None)
else:
flash("Updating your password failed, please try again.", "error")
except AuthApiError as exception:
err = exception.to_dict()
flash(err.get("message"), "error")

return render_template("account/identities.html",
profile=profile,
identities=identities,
connected_identities=connected_identities,
)

@account.route("/connect/github")
@login_required
def link_github():
resp = supabase.auth.link_identity(
{
"provider": "github",
"options": {"redirect_to": f"{request.host_url}auth/callback?next=account.list_identities"},
}
)
flash(f"{'github'.title()} was successfully linked.", "info")

return redirect(resp.url)

@account.route("/connect/<provider>/disconnect")
@login_required
def unlink_provider(provider):
try:
res = supabase.auth.get_user_identities()
identity = list(
filter(lambda identity: identity.provider == provider, res.identities)
).pop()
res = supabase.auth.unlink_identity(identity)
flash(f"{identity.provider.title()} was successfully unlinked.", "info")
except AuthApiError as exception:
err = exception.to_dict()
flash(err.get("message"), "error")

return redirect(url_for("account.list_identities"))

@account.route("/delete/confirm")
@login_required
def destroy():
profile = get_profile_by_user()
return render_template("account/delete.html", profile=profile)

@account.route("/delete", methods=["POST"])
@account.route("/delete/confirm", methods=["POST"])
@login_required
def delete_account():
def destroy_confirm():
form = FlaskForm()
if form.is_submitted():
try:
Expand Down
25 changes: 15 additions & 10 deletions app/auth.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import os
from flask import Blueprint, render_template, redirect, request, session, url_for, flash
from pydantic import ValidationError
from supabase_auth import VerifyEmailOtpParams, VerifyTokenHashParams
from app.forms import AuthForm, ForgotPasswordForm, VerifyTokenForm
from app.supabase import supabase
from gotrue.errors import AuthApiError
from supabase import AuthApiError

auth = Blueprint("auth", __name__, url_prefix="/auth")
supabase_key = os.environ.get("SUPABASE_KEY", "")
Expand All @@ -22,7 +24,7 @@ def signin():
)

if user:
return redirect(url_for(next or "dashboard"))
return redirect(url_for(next or "notes.home"))
except AuthApiError as message:
flash(message, "error")

Expand Down Expand Up @@ -92,33 +94,36 @@ def forgot_password():
@auth.route("/confirm")
def confirm():
token_hash = request.args.get("token_hash")
auth_type = request.args.get("type")
next = request.args.get("next", "dashboard")
auth_type = request.args.get("type", "email")
next = request.args.get("next", "notes.home")

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

supabase.auth.verify_otp(params={"token_hash": token_hash, "type": auth_type})
try:
_ = supabase.auth.verify_otp(params=VerifyTokenHashParams(token_hash=token_hash, type=auth_type))
except ValidationError as exception:
flash("Validation error, please contact support", "error")

return redirect(url_for(next))


@auth.route("/callback")
def callback():
code = request.args.get("code")
next = request.args.get("next", "dashboard")
next = request.args.get("next", "notes.home")

if code:
res = supabase.auth.exchange_code_for_session({"auth_code": code})
_ = supabase.auth.exchange_code_for_session({"auth_code": code})

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", "dashboard")
next = request.args.get("next", "notes.home")
form = VerifyTokenForm()
if form.validate_on_submit():
email = form.email.data
Expand All @@ -129,8 +134,8 @@ def verify_token():
session["password_update_required"] = True

try:
supabase.auth.verify_otp(
params={"email": email, "token": token, "type": auth_type}
_ = supabase.auth.verify_otp(
params=VerifyEmailOtpParams(email=email, token=token, type=auth_type)
)
return redirect(url_for(next))
except AuthApiError as exception:
Expand Down
4 changes: 2 additions & 2 deletions app/decorators.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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 UserResponse
from supabase import AuthApiError, AuthRetryableError
from supabase_auth.types import UserResponse
from app.supabase import get_profile_by_user, supabase


Expand Down
2 changes: 1 addition & 1 deletion app/flask_storage.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from gotrue import SyncSupportedStorage
from supabase_auth import SyncSupportedStorage
from flask import session


Expand Down
23 changes: 23 additions & 0 deletions app/notes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io
from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask_wtf import FlaskForm
from postgrest.exceptions import APIError
from app.forms import NoteForm
from app.supabase import (
Expand Down Expand Up @@ -164,3 +165,25 @@ def edit(note_id):
note=note,
preview_image=preview_image,
)

@notes.route("/<note_id>/delete", methods=["POST"])
@login_required
def delete_note(note_id):
form = FlaskForm()
if form.is_submitted():
try:
r = supabase.table("notes").delete().eq("id", note_id).execute()

if r.data:
flash("Your note has been successfully deleted.", "info")
return redirect(url_for("notes.home"))
else:
flash(
"We couldn't delete your note, please contact support.", "error"
)
return redirect(url_for("notes.home"))
except APIError as exception:
err = exception.to_dict()
flash(err.get("message"), "error")
return redirect(url_for("notes.home"))

13 changes: 9 additions & 4 deletions app/supabase.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import os
from flask import g
from werkzeug.local import LocalProxy
from supabase.client import Client, ClientOptions
from supabase import (
create_client,
Client,
ClientOptions,
AuthApiError,
AuthRetryableError,
)
from supabase_auth import User
from app.flask_storage import FlaskSessionStorage
from gotrue.errors import AuthApiError, AuthRetryableError
from gotrue.types import User
from typing import Union

url = os.environ.get("SUPABASE_URL", "")
Expand All @@ -14,7 +19,7 @@

def get_supabase() -> Client:
if "supabase" not in g:
g.supabase = Client(
g.supabase = create_client(
url,
key,
options=ClientOptions(storage=FlaskSessionStorage(), flow_type="pkce"),
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"build": "tailwindcss -m -i ./tailwind.css -o static/app.css",
"s:start": "supabase start",
"s:stop": "supabase stop",
"s:version": "supabase --version",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
Expand All @@ -15,6 +16,6 @@
"tailwindcss": "^3.3.3"
},
"devDependencies": {
"supabase": "^1.99.5"
"supabase": "2.54.11"
}
}
Loading