Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add AdminAPI and deleteUser method #224

Merged
merged 3 commits into from
Jan 27, 2024
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
1 change: 1 addition & 0 deletions Examples/UserManagement/AuthView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Supabase
import SwiftUI

@MainActor
struct AuthView: View {
@State var email = ""
@State var isLoading = false
Expand Down
21 changes: 20 additions & 1 deletion Examples/UserManagement/ProfileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import PhotosUI
import Supabase
import SwiftUI

@MainActor
struct ProfileView: View {
@State var username = ""
@State var fullName = ""
Expand Down Expand Up @@ -69,6 +70,10 @@ struct ProfileView: View {
if isLoading {
ProgressView()
}

Button("Delete account", role: .destructive) {
deleteAccountButtonTapped()
}
}
}
.onMac { $0.padding() }
Expand All @@ -82,7 +87,7 @@ struct ProfileView: View {
}
}
})
.onChange(of: imageSelection) { newValue in
.onChange(of: imageSelection) { _, newValue in
guard let newValue else { return }
loadTransferable(from: newValue)
}
Expand Down Expand Up @@ -174,6 +179,20 @@ struct ProfileView: View {

return filePath
}

private func deleteAccountButtonTapped() {
Task {
do {
let currentUserId = try await supabase.auth.session.user.id
try await supabase.auth.admin.deleteUser(
id: currentUserId.uuidString,
shouldSoftDelete: true
)
} catch {
debugPrint(error)
}
}
}
}

#Preview {
Expand Down
4 changes: 2 additions & 2 deletions Examples/UserManagement/Supabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import OSLog
import Supabase

let supabase = SupabaseClient(
supabaseURL: URL(string: "https://PROJECT_ID.supabase.co")!,
supabaseKey: "YOUR_SUPABASE_ANON_KEY",
supabaseURL: URL(string: "http://127.0.0.1:54321")!,
supabaseKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU",
options: .init(
global: .init(logger: AppLogger())
)
Expand Down
4 changes: 4 additions & 0 deletions Examples/UserManagement/supabase/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Supabase
.branches
.temp
.env
159 changes: 159 additions & 0 deletions Examples/UserManagement/supabase/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# A string used to distinguish different Supabase projects on the same host. Defaults to the
# working directory name when running `supabase init`.
project_id = "UserManagement"

[api]
enabled = true
# Port to use for the API URL.
port = 54321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. public and storage are always included.
schemas = ["public", "storage", "graphql_public"]
# Extra schemas to add to the search_path of every request. public is always included.
extra_search_path = ["public", "extensions"]
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
# for accidental or malicious requests.
max_rows = 1000

[db]
# Port to use for the local database URL.
port = 54322
# Port used by db diff command to initialize the shadow database.
shadow_port = 54320
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
# server_version;` on the remote database to check.
major_version = 15

[db.pooler]
enabled = false
# Port to use for the local connection pooler.
port = 54329
# Specifies when a server connection can be reused by other clients.
# Configure one of the supported pooler modes: `transaction`, `session`.
pool_mode = "transaction"
# How many server connections to allow per user/database pair.
default_pool_size = 20
# Maximum number of client connections allowed.
max_client_conn = 100

[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv6)
# ip_version = "IPv6"
# The maximum length in bytes of HTTP request headers. (default: 4096)
# max_header_length = 4096

[studio]
enabled = true
# Port to use for Supabase Studio.
port = 54323
# External URL of the API server that frontend connects to.
api_url = "http://127.0.0.1"

# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
# are monitored, and you can view the emails that would have been sent from the web interface.
[inbucket]
enabled = true
# Port to use for the email testing server web interface.
port = 54324
# Uncomment to expose additional ports for testing user applications that send emails.
# smtp_port = 54325
# pop3_port = 54326

[storage]
enabled = true
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"

[auth]
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000", "io.supabase.user-management://*"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# If disabled, the refresh token will never expire.
enable_refresh_token_rotation = true
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
# Requires enable_refresh_token_rotation = true.
refresh_token_reuse_interval = 10
# Allow/disallow new user signups to your project.
enable_signup = true
# Allow/disallow testing manual linking of accounts
enable_manual_linking = false

[auth.email]
# Allow/disallow new user signups via email to your project.
enable_signup = true
# If enabled, a user will be required to confirm any email change on both the old, and new email
# addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false

# Uncomment to customize email template
# [auth.email.template.invite]
# subject = "You have been invited"
# content_path = "./supabase/templates/invite.html"

[auth.sms]
# Allow/disallow new user signups via SMS to your project.
enable_signup = true
# If enabled, users need to confirm their phone number before signing in.
enable_confirmations = false
# Template for sending OTP to users
template = "Your code is {{ .Code }} ."

# Use pre-defined map of phone number to OTP for testing.
[auth.sms.test_otp]
# 4152127777 = "123456"

# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
[auth.hook.custom_access_token]
# enabled = true
# uri = "pg-functions://<database>/<schema>/<hook_name>"


# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
[auth.sms.twilio]
enabled = false
account_sid = ""
message_service_sid = ""
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"

# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
[auth.external.apple]
enabled = false
client_id = ""
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
# Overrides the default auth redirectUrl.
redirect_uri = ""
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
# or any other third-party OIDC providers.
url = ""

[analytics]
enabled = false
port = 54327
vector_port = 54328
# Configure one of the supported backends: `postgres`, `bigquery`.
backend = "postgres"

# Experimental features may be deprecated any time
[experimental]
# Configures Postgres storage engine to use OrioleDB (S3)
orioledb_version = ""
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
s3_host = "env(S3_HOST)"
# Configures S3 bucket region, eg. us-east-1
s3_region = "env(S3_REGION)"
# Configures AWS_ACCESS_KEY_ID for S3 bucket
s3_access_key = "env(S3_ACCESS_KEY)"
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = "env(S3_SECRET_KEY)"
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
-- Create a table for public profiles
create table profiles(
id uuid references auth.users not null primary key,
updated_at timestamp with time zone,
username text unique,
full_name text,
avatar_url text,
website text,
constraint username_length check (char_length(username) >= 3)
);

-- Set up Row Level Security (RLS)
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
alter table profiles enable row level security;

create policy "Public profiles are viewable by everyone." on profiles
for select
using (true);

create policy "Users can insert their own profile." on profiles
for insert
with check (auth.uid() = id);

create policy "Users can update own profile." on profiles
for update
using (auth.uid() = id);

-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
create function public.handle_new_user()
returns trigger
as $$
begin
insert into public.profiles(id, full_name, avatar_url)
values(new.id, new.raw_user_meta_data ->> 'full_name', new.raw_user_meta_data ->> 'avatar_url');
return new;
end;
$$
language plpgsql
security definer;

create trigger on_auth_user_created
after insert on auth.users for each row
execute procedure public.handle_new_user();

-- Set up Storage!
insert into storage.buckets(id, name)
values ('avatars', 'avatars');

-- Set up access controls for storage.
-- See https://supabase.com/docs/guides/storage/security/access-control#policy-examples for more details.
create policy "Avatar images are publicly accessible." on storage.objects
for select
using (bucket_id = 'avatars');

create policy "Anyone can upload an avatar." on storage.objects
for insert
with check (bucket_id = 'avatars');

create policy "Anyone can update their own avatar." on storage.objects
for update
using (auth.uid() = owner)
with check (bucket_id = 'avatars');

Empty file.
36 changes: 36 additions & 0 deletions Sources/Auth/AuthAdmin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// AuthAdmin.swift
//
//
// Created by Guilherme Souza on 25/01/24.
//

import Foundation
@_spi(Internal) import _Helpers

public actor AuthAdmin {
private var configuration: AuthClient.Configuration {
Dependencies.current.value!.configuration
}

private var api: APIClient {
Dependencies.current.value!.api
}

/// Delete a user. Requires `service_role` key.
/// - Parameter id: The id of the user you want to delete.
/// - Parameter shouldSoftDelete: If true, then the user will be soft-deleted (setting
/// `deleted_at` to the current timestamp and disabling their account while preserving their data)
/// from the auth schema.
///
/// - Warning: Never expose your `service_role` key on the client.
public func deleteUser(id: String, shouldSoftDelete: Bool = false) async throws {
_ = try await api.execute(
Request(
path: "/admin/users/\(id)",
method: .delete,
body: configuration.encoder.encode(DeleteUserRequest(shouldSoftDelete: shouldSoftDelete))
)
)
}
}
6 changes: 6 additions & 0 deletions Sources/Auth/AuthClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ public actor AuthClient {
/// Namespace for accessing multi-factor authentication API.
public let mfa: AuthMFA

/// Namespace for the GoTrue admin methods.
/// - Warning: This methods requires `service_role` key, be careful to never expose `service_role`
/// key in the client.
public let admin: AuthAdmin

/// Initializes a AuthClient with optional parameters.
///
/// - Parameters:
Expand Down Expand Up @@ -162,6 +167,7 @@ public actor AuthClient {
logger: SupabaseLogger?
) {
mfa = AuthMFA()
admin = AuthAdmin()

Dependencies.current.setValue(
Dependencies(
Expand Down
14 changes: 12 additions & 2 deletions Sources/Auth/Internal/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,21 @@ extension APIClient {
let response = try await http.fetch(request, baseURL: configuration.url)

guard (200 ..< 300).contains(response.statusCode) else {
let apiError = try configuration.decoder.decode(
if let apiError = try? configuration.decoder.decode(
AuthError.APIError.self,
from: response.data
) {
throw AuthError.api(apiError)
}

/// There are some GoTrue endpoints that can return a `PostgrestError`, for example the
/// ``AuthAdmin/deleteUser(id:shouldSoftDelete:)`` that could return an error in case the
/// user is referenced by other schemas.
let postgrestError = try configuration.decoder.decode(
PostgrestError.self,
from: response.data
)
throw AuthError.api(apiError)
throw postgrestError
}

return response
Expand Down
4 changes: 4 additions & 0 deletions Sources/Auth/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -646,3 +646,7 @@ public struct WeakPassword: Codable, Hashable, Sendable {
/// `pwned`.
public let reasons: [String]
}

struct DeleteUserRequest: Encodable {
let shouldSoftDelete: Bool
}
Loading
Loading