Skip to content

Commit 04661f8

Browse files
authored
feat(auth): add missing auth admin methods (#715)
* feat(auth): add missing auth admin methods * add tests for admin features * make ids an uuid * deprecate deleteUser with id string * remove renamed * remove CodingKeys * wip * decode GenerateLinkResponse * comment generate link related code * rollback AnyJSON encoder and decoder
1 parent 3e292c5 commit 04661f8

File tree

13 files changed

+617
-55
lines changed

13 files changed

+617
-55
lines changed

Examples/UserManagement/ProfileView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ struct ProfileView: View {
185185
do {
186186
let currentUserId = try await supabase.auth.session.user.id
187187
try await supabase.auth.admin.deleteUser(
188-
id: currentUserId.uuidString,
188+
id: currentUserId,
189189
shouldSoftDelete: true
190190
)
191191
} catch {

Sources/Auth/AuthAdmin.swift

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
//
77

88
import Foundation
9-
import Helpers
109
import HTTPTypes
10+
import Helpers
1111

1212
public struct AuthAdmin: Sendable {
1313
let clientID: AuthClientID
@@ -16,14 +16,97 @@ public struct AuthAdmin: Sendable {
1616
var api: APIClient { Dependencies[clientID].api }
1717
var encoder: JSONEncoder { Dependencies[clientID].encoder }
1818

19+
/// Get user by id.
20+
/// - Parameter uid: The user's unique identifier.
21+
/// - Note: This function should only be called on a server. Never expose your `service_role` key in the browser.
22+
public func getUserById(_ uid: UUID) async throws -> User {
23+
try await api.execute(
24+
HTTPRequest(
25+
url: configuration.url.appendingPathComponent("admin/users/\(uid)"),
26+
method: .get
27+
)
28+
).decoded(decoder: configuration.decoder)
29+
}
30+
31+
/// Updates the user data.
32+
/// - Parameters:
33+
/// - uid: The user id you want to update.
34+
/// - attributes: The data you want to update.
35+
@discardableResult
36+
public func updateUserById(_ uid: UUID, attributes: AdminUserAttributes) async throws -> User {
37+
try await api.execute(
38+
HTTPRequest(
39+
url: configuration.url.appendingPathComponent("admin/users/\(uid)"),
40+
method: .put,
41+
body: configuration.encoder.encode(attributes)
42+
)
43+
).decoded(decoder: configuration.decoder)
44+
}
45+
46+
/// Creates a new user.
47+
///
48+
/// - To confirm the user's email address or phone number, set ``AdminUserAttributes/emailConfirm`` or ``AdminUserAttributes/phoneConfirm`` to `true`. Both arguments default to `false`.
49+
/// - ``createUser(attributes:)`` will not send a confirmation email to the user. You can use ``inviteUserByEmail(_:data:redirectTo:)`` if you want to send them an email invite instead.
50+
/// - If you are sure that the created user's email or phone number is legitimate and verified, you can set the ``AdminUserAttributes/emailConfirm`` or ``AdminUserAttributes/phoneConfirm`` param to true.
51+
/// - Warning: Never expose your `service_role` key on the client.
52+
@discardableResult
53+
public func createUser(attributes: AdminUserAttributes) async throws -> User {
54+
try await api.execute(
55+
HTTPRequest(
56+
url: configuration.url.appendingPathComponent("admin/users"),
57+
method: .post,
58+
body: encoder.encode(attributes)
59+
)
60+
)
61+
.decoded(decoder: configuration.decoder)
62+
}
63+
64+
/// Sends an invite link to an email address.
65+
///
66+
/// - Sends an invite link to the user's email address.
67+
/// - The ``inviteUserByEmail(_:data:redirectTo:)`` method is typically used by administrators to invite users to join the application.
68+
/// - Parameters:
69+
/// - email: The email address of the user.
70+
/// - data: A custom data object to store additional metadata about the user. This maps to the `auth.users.user_metadata` column.
71+
/// - redirectTo: The URL which will be appended to the email link sent to the user's email address. Once clicked the user will end up on this URL.
72+
/// - Note: that PKCE is not supported when using ``inviteUserByEmail(_:data:redirectTo:)``. This is because the browser initiating the invite is often different from the browser accepting the invite which makes it difficult to provide the security guarantees required of the PKCE flow.
73+
@discardableResult
74+
public func inviteUserByEmail(
75+
_ email: String,
76+
data: [String: AnyJSON]? = nil,
77+
redirectTo: URL? = nil
78+
) async throws -> User {
79+
try await api.execute(
80+
HTTPRequest(
81+
url: configuration.url.appendingPathComponent("admin/invite"),
82+
method: .post,
83+
query: [
84+
(redirectTo ?? configuration.redirectToURL).map {
85+
URLQueryItem(
86+
name: "redirect_to",
87+
value: $0.absoluteString
88+
)
89+
}
90+
].compactMap { $0 },
91+
body: encoder.encode(
92+
[
93+
"email": .string(email),
94+
"data": data.map({ AnyJSON.object($0) }) ?? .null,
95+
]
96+
)
97+
)
98+
)
99+
.decoded(decoder: configuration.decoder)
100+
}
101+
19102
/// Delete a user. Requires `service_role` key.
20103
/// - Parameter id: The id of the user you want to delete.
21104
/// - Parameter shouldSoftDelete: If true, then the user will be soft-deleted (setting
22105
/// `deleted_at` to the current timestamp and disabling their account while preserving their data)
23106
/// from the auth schema.
24107
///
25108
/// - Warning: Never expose your `service_role` key on the client.
26-
public func deleteUser(id: String, shouldSoftDelete: Bool = false) async throws {
109+
public func deleteUser(id: UUID, shouldSoftDelete: Bool = false) async throws {
27110
_ = try await api.execute(
28111
HTTPRequest(
29112
url: configuration.url.appendingPathComponent("admin/users/\(id)"),
@@ -69,7 +152,9 @@ public struct AuthAdmin: Sendable {
69152
let links = httpResponse.headers[.link]?.components(separatedBy: ",") ?? []
70153
if !links.isEmpty {
71154
for link in links {
72-
let page = link.components(separatedBy: ";")[0].components(separatedBy: "=")[1].prefix(while: \.isNumber)
155+
let page = link.components(separatedBy: ";")[0].components(separatedBy: "=")[1].prefix(
156+
while: \.isNumber
157+
)
73158
let rel = link.components(separatedBy: ";")[1].components(separatedBy: "=")[1]
74159

75160
if rel == "\"last\"", let lastPage = Int(page) {
@@ -82,6 +167,35 @@ public struct AuthAdmin: Sendable {
82167

83168
return pagination
84169
}
170+
171+
/*
172+
Generate link is commented out temporarily due issues with they Auth's decoding is configured.
173+
Will revisit it later.
174+
175+
/// Generates email links and OTPs to be sent via a custom email provider.
176+
///
177+
/// - Parameter params: The parameters for the link generation.
178+
/// - Throws: An error if the link generation fails.
179+
/// - Returns: The generated link.
180+
public func generateLink(params: GenerateLinkParams) async throws -> GenerateLinkResponse {
181+
try await api.execute(
182+
HTTPRequest(
183+
url: configuration.url.appendingPathComponent("admin/generate_link").appendingQueryItems(
184+
[
185+
(params.redirectTo ?? configuration.redirectToURL).map {
186+
URLQueryItem(
187+
name: "redirect_to",
188+
value: $0.absoluteString
189+
)
190+
}
191+
].compactMap { $0 }
192+
),
193+
method: .post,
194+
body: encoder.encode(params.body)
195+
)
196+
).decoded(decoder: configuration.decoder)
197+
}
198+
*/
85199
}
86200

87201
extension HTTPField.Name {

Sources/Auth/Deprecated.swift

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ extension JSONEncoder {
3232
*,
3333
deprecated,
3434
renamed: "AuthClient.Configuration.jsonEncoder",
35-
message: "Access to the default JSONEncoder instance moved to AuthClient.Configuration.jsonEncoder"
35+
message:
36+
"Access to the default JSONEncoder instance moved to AuthClient.Configuration.jsonEncoder"
3637
)
3738
public static var goTrue: JSONEncoder {
3839
AuthClient.Configuration.jsonEncoder
@@ -44,7 +45,8 @@ extension JSONDecoder {
4445
*,
4546
deprecated,
4647
renamed: "AuthClient.Configuration.jsonDecoder",
47-
message: "Access to the default JSONDecoder instance moved to AuthClient.Configuration.jsonDecoder"
48+
message:
49+
"Access to the default JSONDecoder instance moved to AuthClient.Configuration.jsonDecoder"
4850
)
4951
public static var goTrue: JSONDecoder {
5052
AuthClient.Configuration.jsonDecoder
@@ -65,7 +67,8 @@ extension AuthClient.Configuration {
6567
@available(
6668
*,
6769
deprecated,
68-
message: "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)"
70+
message:
71+
"Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)"
6972
)
7073
public init(
7174
url: URL,
@@ -103,7 +106,8 @@ extension AuthClient {
103106
@available(
104107
*,
105108
deprecated,
106-
message: "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)"
109+
message:
110+
"Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)"
107111
)
108112
public init(
109113
url: URL,
@@ -129,3 +133,18 @@ extension AuthClient {
129133

130134
@available(*, deprecated, message: "Use MFATotpEnrollParams or MFAPhoneEnrollParams instead.")
131135
public typealias MFAEnrollParams = MFATotpEnrollParams
136+
137+
extension AuthAdmin {
138+
@available(
139+
*,
140+
deprecated,
141+
message: "Use deleteUser with UUID instead of string."
142+
)
143+
public func deleteUser(id: String, shouldSoftDelete: Bool = false) async throws {
144+
guard let id = UUID(uuidString: id) else {
145+
fatalError("id should be a valid UUID")
146+
}
147+
148+
try await self.deleteUser(id: id, shouldSoftDelete: shouldSoftDelete)
149+
}
150+
}

Sources/Auth/Internal/APIClient.swift

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
2-
import Helpers
32
import HTTPTypes
3+
import Helpers
44

55
extension HTTPClient {
66
init(configuration: AuthClient.Configuration) {
@@ -12,7 +12,7 @@ extension HTTPClient {
1212
interceptors.append(
1313
RetryRequestInterceptor(
1414
retryableHTTPMethods: RetryRequestInterceptor.defaultRetryableHTTPMethods.union(
15-
[.post] // Add POST method so refresh token are also retried.
15+
[.post] // Add POST method so refresh token are also retried.
1616
)
1717
)
1818
)
@@ -42,7 +42,7 @@ struct APIClient: Sendable {
4242

4343
let response = try await http.send(request)
4444

45-
guard 200 ..< 300 ~= response.statusCode else {
45+
guard 200..<300 ~= response.statusCode else {
4646
throw handleError(response: response)
4747
}
4848

@@ -64,10 +64,12 @@ struct APIClient: Sendable {
6464
}
6565

6666
func handleError(response: Helpers.HTTPResponse) -> AuthError {
67-
guard let error = try? response.decoded(
68-
as: _RawAPIErrorResponse.self,
69-
decoder: configuration.decoder
70-
) else {
67+
guard
68+
let error = try? response.decoded(
69+
as: _RawAPIErrorResponse.self,
70+
decoder: configuration.decoder
71+
)
72+
else {
7173
return .api(
7274
message: "Unexpected error",
7375
errorCode: .unexpectedFailure,
@@ -78,11 +80,14 @@ struct APIClient: Sendable {
7880

7981
let responseAPIVersion = parseResponseAPIVersion(response)
8082

81-
let errorCode: ErrorCode? = if let responseAPIVersion, responseAPIVersion >= apiVersions[._20240101]!.timestamp, let code = error.code {
82-
ErrorCode(code)
83-
} else {
84-
error.errorCode
85-
}
83+
let errorCode: ErrorCode? =
84+
if let responseAPIVersion, responseAPIVersion >= apiVersions[._20240101]!.timestamp,
85+
let code = error.code
86+
{
87+
ErrorCode(code)
88+
} else {
89+
error.errorCode
90+
}
8691

8792
if errorCode == nil, let weakPassword = error.weakPassword {
8893
return .weakPassword(

0 commit comments

Comments
 (0)