Skip to content

Commit

Permalink
Merge pull request #15 from silentworks/main
Browse files Browse the repository at this point in the history
Merge main into develop
  • Loading branch information
silentworks authored Oct 1, 2023
2 parents 5384054 + fcf4912 commit 0e22b73
Show file tree
Hide file tree
Showing 26 changed files with 1,166 additions and 215 deletions.
14 changes: 8 additions & 6 deletions .github/workflows/production.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
name: Deploy Migrations to Production
name: Deploy Migrations/Edge Functions to Production

on:
push:
branches:
- main
workflow_dispatch:
workflow_dispatch

jobs:
deploy:
Expand All @@ -23,4 +20,9 @@ jobs:
version: latest

- run: supabase link --project-ref $SUPABASE_PROJECT_ID
- run: supabase db push

- name: Database Migrations push
run: supabase db push

- name: Edge Functions deployment
run: supabase functions deploy --project-ref $SUPABASE_PROJECT_ID
31 changes: 31 additions & 0 deletions .github/workflows/staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Deploy Migrations/Edge Functions to Staging

on:
push:
branches:
- develop
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
environment: database
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_DB_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}
SUPABASE_PROJECT_ID: ${{ secrets.STAGING_PROJECT_ID }}

steps:
- uses: actions/checkout@v3

- uses: supabase/setup-cli@v1
with:
version: latest

- run: supabase link --project-ref $SUPABASE_PROJECT_ID

- name: Database Migrations push
run: supabase db push

- name: Edge Functions deployment
run: supabase functions deploy --project-ref $SUPABASE_PROJECT_ID
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# Python Flask Demo
# Flask Notes

This is a Flask/Supabase project showing how to create a user profile along with how to store sensitive data that only the user of that data should be able to view using a one-to-one relationship and row level security (RLS). This project also demonstrates how to use a Postgres function to update two tables (which is done in a transaction so that if one fails there should be a rollback) using a `.rpc` function call. We also demonstrate how to use a generated column for the slug inside the database by making use of a Postgres function we create.
This is a Flask/Supabase project showing how to create a user profile along with how to store sensitive data that only the user of that data should be able to view using a one-to-one relationship and row level security (RLS). This project also demonstrates how to use a Postgres function to update two tables (which is done in a transaction so that if one fails there should be a rollback) using a `.rpc` function call. We also demonstrate how to use a generated column for the slug inside the database by making use of a Postgres function we create. Storage is used to store the featured image for the notes in the app.

This project makes use of:

- [Supabase Auth Helpers SvelteKit](https://supabase.com/docs/guides/auth/auth-helpers/sveltekit)
- [Supabase Python Library](https://supabase.com/docs/reference/python/introduction)
- [Poetry](https://python-poetry.org/)
- [Flask](https://flask.palletsprojects.com/en/3.0.x/)
- [DaisyUI](https://daisyui.com/)
- [tailwindcss](https://tailwindcss.com/)
- [pgTAP](https://pgtap.org/) Postgres unit testing
- [Tailwind Profile from Codepen](https://codepen.io/ScottWindon/pen/XWdbPLm)
- [heroicons](https://heroicons.com/)

## Getting started

Expand All @@ -17,9 +20,10 @@ You can get started with this locally by using the Supabase CLI. Make sure you h
Create a copy of this project using the commands below:

```bash
npx degit silentworks/supabase-flask-demo project-name
npx degit silentworks/flask-notes project-name
cd project-name
npm install # or pnpm install or yarn install
poetry install
```

Run the command below to start your local Supabase docker instance
Expand Down
47 changes: 43 additions & 4 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,76 @@
from flask import Flask, abort, render_template
from flask import Flask, g, render_template
from flask_misaka import Misaka
from app.supabase import (
supabase,
session_context_processor,
get_profile_by_slug,
get_profile_by_user,
get_all_notes_by_user_id,
get_note_by_slug,
get_all_notes_with_profile,
)
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
from app.utils import humanize_ts, resize_image

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

Misaka(app)

# Set the secret key to some random bytes. Keep this really secret!
app.secret_key = b"c8af64a6a0672678800db3c5a3a8d179f386e083f559518f2528202a4b7de8f8"
app.context_processor(session_context_processor)
app.register_blueprint(auth)
app.register_blueprint(account)
app.register_blueprint(notes)
app.jinja_env.filters["humanize"] = humanize_ts


@app.teardown_appcontext
def close_supabase(e=None):
g.pop("supabase", None)


@app.route("/")
def home():
notes = get_all_notes_with_profile()
return render_template("index.html", notes=notes)


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


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


@app.route("/u/<slug>/<note_slug>")
def user_note(slug, note_slug):
profile = get_profile_by_slug(slug)
note = get_note_by_slug(note_slug)
featured_image = None
try:
r = supabase.storage.from_("featured_image").get_public_url(
note["featured_image"]
)
featured_image = resize_image(r, 700, 400)
except:
None

return render_template(
"note.html", profile=profile, note=note, featured_image=featured_image
)


@app.route("/service-unavailable")
Expand Down
27 changes: 16 additions & 11 deletions app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def signin():
)

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

Expand All @@ -36,22 +36,27 @@ def signup():
email = form.email.data
password = form.password.data

user = supabase.auth.sign_up(credentials={"email": email, "password": password})
try:
user = supabase.auth.sign_up(
credentials={"email": email, "password": password}
)

if user:
return redirect(url_for("home"))
else:
flash("User registration failed!", "error")
if user:
flash(
"Please check your email for a magic link to log into the website.",
"info",
)
else:
flash("User registration failed!", "error")
except AuthApiError as message:
flash(message, "error")

return render_template("auth/signup.html", form=form)


@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 All @@ -74,7 +79,7 @@ def forgot_password():
def confirm():
token_hash = request.args.get("token_hash")
auth_type = request.args.get("type")
next = request.args.get("next", "home")
next = request.args.get("next", "dashboard")

if token_hash and auth_type:
if auth_type == "recovery":
Expand All @@ -88,7 +93,7 @@ def confirm():
@auth.route("/verify-token", methods=["GET", "POST"])
def verify_token():
auth_type = request.args.get("type", "email")
next = request.args.get("next", "home")
next = request.args.get("next", "dashboard")
form = VerifyTokenForm()
if form.validate_on_submit():
email = form.email.data
Expand Down
3 changes: 0 additions & 3 deletions app/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ 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":
Expand Down
55 changes: 29 additions & 26 deletions app/notes.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import base64
import io
import requests
from flask import Blueprint, flash, redirect, render_template, request, url_for
from PIL import Image, ImageOps
from urllib.request import urlopen
from postgrest.exceptions import APIError
from app.forms import NoteForm
from app.supabase import (
Expand All @@ -12,7 +12,7 @@
get_profile_by_user,
)
from app.decorators import login_required, password_update_required, profile_required
from app.utils import random_choice
from app.utils import random_choice, resize_image

notes = Blueprint("notes", __name__, url_prefix="/notes")

Expand All @@ -34,23 +34,27 @@ def home():
def new():
profile = get_profile_by_user()
form = NoteForm(data=profile)
path = None
if form.validate_on_submit():
title = form.title.data
content = form.content.data
is_public = form.is_public.data
featured_image = form.featured_image.name
image = request.files[featured_image]
image_stream = io.BytesIO()
image.save(image_stream)
path = f"{profile['id']}/{random_choice().lower()}_fi.png"
# check if image is set
if image:
image_stream = io.BytesIO()
image.save(image_stream)
path = f"{profile['id']}/{random_choice().lower()}_fi.png"

try:
# Upload file
r = supabase.storage.from_("featured_image").upload(
path=path,
file=image_stream.getvalue(),
file_options={"content-type": image.content_type},
)
if path is not None:
r = supabase.storage.from_("featured_image").upload(
path=path,
file=image_stream.getvalue(),
file_options={"content-type": image.content_type},
)

# Save to database
res = (
Expand Down Expand Up @@ -89,9 +93,10 @@ def edit(note_id):
profile = get_profile_by_user()
note = get_note_by_user_and_id(note_id)
form = NoteForm(data=note)
image = None
if note["featured_image"] is not None:
# A Supabase PRO plan is required to use image transforms
preview_image = None
path = note["featured_image"]
if path is not None:
# A Supabase PRO plan is required to use the image transform below
# r = supabase.storage.from_("featured_image").get_public_url(
# note["featured_image"],
# options={"transform": {"width": 200}},
Expand All @@ -101,32 +106,26 @@ def edit(note_id):
# the burden will be on our own server rather than Supabase's and we
# have to write a lot more code to accomplish the transform, you may
# also want to cache this as this action will be performed everytime
# you load an image
# you load an image. Pillow transform code can be found in the
# resize_image function below
r = supabase.storage.from_("featured_image").get_public_url(
note["featured_image"]
)
url_to_stream = urlopen(r)
img = Image.open(url_to_stream)
img = ImageOps.contain(img, (200, 200))
image_stream = io.BytesIO()
img.save(image_stream, format="png")
image = f"data:image/png;base64, {base64.b64encode(image_stream.getvalue()).decode('utf-8')}"
else:
path = note["featured_image"]
preview_image = resize_image(r, 200, 200)

if form.validate_on_submit():
title = form.title.data
content = form.content.data
is_public = form.is_public.data
featured_image = form.featured_image.name
if featured_image is not None:
image = request.files[featured_image]
image = request.files[featured_image]
if image:
image_stream = io.BytesIO()
image.save(image_stream)
path = f"{profile['id']}/{random_choice().lower()}_fi.png"

try:
if featured_image is not None:
if image:
# Upload file
r = supabase.storage.from_("featured_image").upload(
path=path,
Expand Down Expand Up @@ -162,5 +161,9 @@ def edit(note_id):
flash(exception.message, "error")

return render_template(
"notes/edit.html", profile=profile, form=form, note=note, image=image
"notes/edit.html",
profile=profile,
form=form,
note=note,
preview_image=preview_image,
)
Loading

0 comments on commit 0e22b73

Please sign in to comment.