Skip to content

Commit

Permalink
Update account deletion with functions-py
Browse files Browse the repository at this point in the history
  • Loading branch information
silentworks committed Oct 31, 2023
1 parent 1170a1f commit 342c685
Show file tree
Hide file tree
Showing 16 changed files with 251 additions and 15 deletions.
24 changes: 24 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Poetry",
"type": "python",
"request": "launch",
"cwd": "${workspaceFolder}",
"module": "flask",
"python": "${workspaceFolder}/.venv/bin/python",
"env": {
"FLASK_APP": "app/__init__.py",
"FLASK_DEBUG": "1"
},
"args": [
"run",
"--no-debugger",
"--no-reload"
],
"jinja": true,
"justMyCode": true
}
]
}
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"deno.enable": true,
"deno.enablePaths": [
"supabase"
"./supabase/functions/"
]
}
12 changes: 8 additions & 4 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from flask import Flask, g, render_template
from flask_misaka import Misaka
from flask import Flask, abort, g, render_template

# from flask_misaka import Misaka
from app.supabase import (
supabase,
session_context_processor,
Expand All @@ -13,11 +14,11 @@
from app.auth import auth
from app.account import account
from app.notes import notes
from app.utils import humanize_ts, resize_image
from app.utils import humanize_ts, mkdown, resize_image

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

Misaka(app)
# Misaka(app)

# Set the secret key to some random bytes. Keep this really secret!
app.secret_key = b"c8af64a6a0672678800db3c5a3a8d179f386e083f559518f2528202a4b7de8f8"
Expand All @@ -26,6 +27,7 @@
app.register_blueprint(account)
app.register_blueprint(notes)
app.jinja_env.filters["humanize"] = humanize_ts
app.jinja_env.filters["markdown"] = mkdown


@app.teardown_appcontext
Expand Down Expand Up @@ -59,6 +61,8 @@ def u(slug):
def user_note(slug, note_slug):
profile = get_profile_by_slug(slug)
note = get_note_by_slug(note_slug)
if note.get("slug") is None:
abort(404)
featured_image = None
try:
r = supabase.storage.from_("featured_image").get_public_url(
Expand Down
27 changes: 26 additions & 1 deletion app/account.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from flask import Blueprint, render_template, flash, request, session
from flask import Blueprint, redirect, render_template, flash, request, session, url_for
from flask_wtf import FlaskForm
from gotrue.errors import AuthApiError
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, session_context_processor, supabase
Expand Down Expand Up @@ -107,3 +109,26 @@ def update_password():
flash(err.get("message"), "error")

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


@account.route("/delete", methods=["POST"])
@login_required
def delete_account():
form = FlaskForm()
if form.is_submitted():
try:
r = supabase.functions.invoke("delete-account")

if r:
flash("Your account has been successfully deleted.", "info")
supabase.auth.sign_out()
return redirect(url_for("auth.signin"))
else:
flash(
"We couldn't delete your account, please contact support.", "error"
)
return redirect(url_for("account.home"))
except (FunctionsRelayError, FunctionsHttpError) as exception:
err = exception.to_dict()
flash(err.get("message"), "error")
return redirect(url_for("account.home"))
5 changes: 4 additions & 1 deletion app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ def signup():

@auth.route("/signout", methods=["POST"])
def signout():
supabase.auth.sign_out()
try:
supabase.auth.sign_out()
except AuthApiError:
None
return redirect(url_for("auth.signin"))


Expand Down
2 changes: 1 addition & 1 deletion app/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Union
from flask import redirect, session, url_for, request
from gotrue.errors import AuthApiError, AuthRetryableError
from gotrue.types import Session
from gotrue.types import Session, User
from app.supabase import get_profile_by_user, supabase


Expand Down
3 changes: 0 additions & 3 deletions app/notes.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import base64
import io
import requests
from flask import Blueprint, flash, redirect, render_template, request, url_for
from PIL import Image, ImageOps
from postgrest.exceptions import APIError
from app.forms import NoteForm
from app.supabase import (
Expand Down
3 changes: 1 addition & 2 deletions app/supabase.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import os
from flask import g
from werkzeug.local import LocalProxy
from supabase.client import create_client, Client
from supabase.lib.client_options import ClientOptions
from supabase.client import create_client, Client, ClientOptions
from app.flask_storage import FlaskSessionStorage
from gotrue.errors import AuthApiError, AuthRetryableError
from gotrue.types import User
Expand Down
5 changes: 5 additions & 0 deletions app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import random
import string
from PIL import Image, ImageOps
import markdown
import requests


Expand Down Expand Up @@ -73,3 +74,7 @@ def humanize_ts(timestamp=False, fmt=False):
if day_diff < 182:
return str(int(day_diff / 30)) + " months ago"
return datetm.strftime(fmt or "%b %d %Y")


def mkdown(text: str):
return markdown.markdown(text)
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"scripts": {
"dev": "tailwindcss -w -i ./tailwind.css -o static/app.css",
"build": "tailwindcss -m -i ./tailwind.css -o static/app.css",
"s:start": "supabase start",
"s:stop": "supabase stop",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
Expand Down
68 changes: 68 additions & 0 deletions static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -2736,6 +2736,10 @@ html {
border-radius: 0px;
}

.border {
border-width: 1px;
}

.border-b {
border-bottom-width: 1px;
}
Expand All @@ -2750,6 +2754,11 @@ html {
border-color: rgb(31 41 55 / var(--tw-border-opacity));
}

.border-red-300 {
--tw-border-opacity: 1;
border-color: rgb(252 165 165 / var(--tw-border-opacity));
}

.bg-base-100 {
--tw-bg-opacity: 1;
background-color: hsl(var(--b1) / var(--tw-bg-opacity));
Expand Down Expand Up @@ -2780,6 +2789,26 @@ html {
background-color: rgb(253 224 71 / var(--tw-bg-opacity));
}

.bg-blue-600 {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
}

.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}

.bg-red-900 {
--tw-bg-opacity: 1;
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
}

.bg-red-700 {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}

.p-12 {
padding: 3rem;
}
Expand All @@ -2792,6 +2821,10 @@ html {
padding: 0.75rem;
}

.p-4 {
padding: 1rem;
}

.px-4 {
padding-left: 1rem;
padding-right: 1rem;
Expand Down Expand Up @@ -2991,6 +3024,31 @@ html {
color: rgb(253 224 71 / var(--tw-text-opacity));
}

.text-blue-200 {
--tw-text-opacity: 1;
color: rgb(191 219 254 / var(--tw-text-opacity));
}

.text-red-400 {
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity));
}

.text-red-800 {
--tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity));
}

.text-red-900 {
--tw-text-opacity: 1;
color: rgb(127 29 29 / var(--tw-text-opacity));
}

.text-red-700 {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity));
}

.underline {
text-decoration-line: underline;
}
Expand Down Expand Up @@ -3023,6 +3081,11 @@ html {
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}

.hover\:bg-red-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}

.hover\:text-blue-200:hover {
--tw-text-opacity: 1;
color: rgb(191 219 254 / var(--tw-text-opacity));
Expand All @@ -3043,6 +3106,11 @@ html {
color: rgb(99 102 241 / var(--tw-text-opacity));
}

.hover\:text-gray-200:hover {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
}

.group:hover .group-hover\:inline {
display: inline;
}
Expand Down
5 changes: 5 additions & 0 deletions supabase/functions/_shared/cors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

95 changes: 95 additions & 0 deletions supabase/functions/delete-account/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.37.0"
import { corsHeaders } from "../_shared/cors.ts";

console.log(`Function "user-self-deletion" up and running!`)

serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}

try {
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
// Create client with Auth context of the user that called the function.
// This way your row-level-security (RLS) policies are applied.
{
global: {
headers: { Authorization: req.headers.get('Authorization')! }
}
}
)

// Now we can get the session or user object
const {
data: { user },
} = await supabaseClient.auth.getUser()
// And we can run queries in the context of our authenticated user
const { data: profile, error: userError } = await supabaseClient.from('profiles')
.select('id')
.match({ id: user?.id })
.single()

if (userError) {
throw userError
}

const user_id = profile.id
const { data: list_of_files, error: storageError } = await supabaseClient
.storage
.from('featured_image')
.list(user_id)

if (storageError) {
throw storageError
}

const file_urls = []
for (let i = 0; i < list_of_files.length; i++) {
file_urls.push(list_of_files[i].name)
}

console.log("Files to delete: ", { file_urls: file_urls.map(name => `${user_id}/${name}`) })

// Create the admin client to delete files & user with the Admin API.
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)

if (file_urls.length > 0) {
const { data: fi, error: fi_error } = await supabaseAdmin
.storage
.from('featured_image')
.remove(file_urls.map(name => `${user_id}/${name}`))

if (fi_error) {
throw fi_error
}
}
// throw new Error(`User & files deleted user_id: ${user_id}`)
const { data: deletion_data, error: deletion_error } = await supabaseAdmin
.auth.admin
.deleteUser(user_id)

if (deletion_error) throw deletion_error
console.log("User & files deleted user_id: " + user_id)
return new Response("User deleted: " + JSON.stringify(deletion_data, null, 2), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
});
} catch (error) {
return Response.json({ error: error.message }, {
headers: { 'Content-Type': 'application/json' },
status: 400
})
}
})

// To invoke:
// curl -i --location --request POST 'http://localhost:54321/functions/v1/delete_account' \
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
// --header 'Content-Type: application/json' \
// --data '{"name":"Functions"}'
Loading

0 comments on commit 342c685

Please sign in to comment.