Skip to content

feat(auth): add missing auth admin methods #715

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
117 changes: 115 additions & 2 deletions Sources/Auth/AuthAdmin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
//

import Foundation
import Helpers
import HTTPTypes
import Helpers

public struct AuthAdmin: Sendable {
let clientID: AuthClientID
Expand All @@ -16,6 +16,89 @@ public struct AuthAdmin: Sendable {
var api: APIClient { Dependencies[clientID].api }
var encoder: JSONEncoder { Dependencies[clientID].encoder }

/// Get user by id.
/// - Parameter uid: The user's unique identifier.
/// - Note: This function should only be called on a server. Never expose your `service_role` key in the browser.
public func getUserById(_ uid: String) async throws -> User {
try await api.execute(
HTTPRequest(
url: configuration.url.appendingPathComponent("admin/users/\(uid)"),
method: .get
)
).decoded(decoder: configuration.decoder)
}

/// Updates the user data.
/// - Parameters:
/// - uid: The user id you want to update.
/// - attributes: The data you want to update.
@discardableResult
public func updateUserById(_ uid: String, attributes: AdminUserAttributes) async throws -> User {
try await api.execute(
HTTPRequest(
url: configuration.url.appendingPathComponent("admin/users/\(uid)"),
method: .put,
body: configuration.encoder.encode(attributes)
)
).decoded(decoder: configuration.decoder)
}

/// Creates a new user.
///
/// - To confirm the user's email address or phone number, set ``AdminUserAttributes/emailConfirm`` or ``AdminUserAttributes/phoneConfirm`` to `true`. Both arguments default to `false`.
/// - ``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.
/// - 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.
/// - Warning: Never expose your `service_role` key on the client.
@discardableResult
public func createUser(attributes: AdminUserAttributes) async throws -> User {
try await api.execute(
HTTPRequest(
url: configuration.url.appendingPathComponent("admin/users"),
method: .post,
body: encoder.encode(attributes)
)
)
.decoded(decoder: configuration.decoder)
}

/// Sends an invite link to an email address.
///
/// - Sends an invite link to the user's email address.
/// - The ``inviteUserByEmail(_:data:redirectTo:)`` method is typically used by administrators to invite users to join the application.
/// - Parameters:
/// - email: The email address of the user.
/// - data: A custom data object to store additional metadata about the user. This maps to the `auth.users.user_metadata` column.
/// - 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.
/// - 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.
@discardableResult
public func inviteUserByEmail(
_ email: String,
data: [String: AnyJSON]? = nil,
redirectTo: URL? = nil
) async throws -> User {
try await api.execute(
HTTPRequest(
url: configuration.url.appendingPathComponent("admin/invite"),
method: .post,
query: [
(redirectTo ?? configuration.redirectToURL).map {
URLQueryItem(
name: "redirect_to",
value: $0.absoluteString
)
}
].compactMap { $0 },
body: encoder.encode(
[
"email": .string(email),
"data": data.map({ AnyJSON.object($0) }) ?? .null,
]
)
)
)
.decoded(decoder: configuration.decoder)
}

/// 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
Expand Down Expand Up @@ -69,7 +152,8 @@ public struct AuthAdmin: Sendable {
let links = httpResponse.headers[.link]?.components(separatedBy: ",") ?? []
if !links.isEmpty {
for link in links {
let page = link.components(separatedBy: ";")[0].components(separatedBy: "=")[1].prefix(while: \.isNumber)
let page = link.components(separatedBy: ";")[0].components(separatedBy: "=")[1].prefix(
while: \.isNumber)
let rel = link.components(separatedBy: ";")[1].components(separatedBy: "=")[1]

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

return pagination
}

/// Generates email links and OTPs to be sent via a custom email provider.
///
/// - Parameter params: The parameters for the link generation.
/// - Throws: An error if the link generation fails.
/// - Returns: The generated link.
public func generateLink(params: GenerateLinkParams) async throws -> GenerateLinkResponse {
let response = try await api.execute(
HTTPRequest(
url: configuration.url.appendingPathComponent("admin/generate_link").appendingQueryItems(
[
(params.redirectTo ?? configuration.redirectToURL).map {
URLQueryItem(
name: "redirect_to",
value: $0.absoluteString
)
}
].compactMap { $0 }
),
method: .post,
body: encoder.encode(params.body)
)
).decoded(as: AnyJSON.self, decoder: configuration.decoder)

let properties = try response.decode(as: GenerateLinkProperties.self)
let user = try response.decode(as: User.self)

return GenerateLinkResponse(properties: properties, user: user)
}
}

extension HTTPField.Name {
Expand Down
29 changes: 17 additions & 12 deletions Sources/Auth/Internal/APIClient.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation
import Helpers
import HTTPTypes
import Helpers

extension HTTPClient {
init(configuration: AuthClient.Configuration) {
Expand All @@ -12,7 +12,7 @@ extension HTTPClient {
interceptors.append(
RetryRequestInterceptor(
retryableHTTPMethods: RetryRequestInterceptor.defaultRetryableHTTPMethods.union(
[.post] // Add POST method so refresh token are also retried.
[.post] // Add POST method so refresh token are also retried.
)
)
)
Expand Down Expand Up @@ -42,7 +42,7 @@ struct APIClient: Sendable {

let response = try await http.send(request)

guard 200 ..< 300 ~= response.statusCode else {
guard 200..<300 ~= response.statusCode else {
throw handleError(response: response)
}

Expand All @@ -64,10 +64,12 @@ struct APIClient: Sendable {
}

func handleError(response: Helpers.HTTPResponse) -> AuthError {
guard let error = try? response.decoded(
as: _RawAPIErrorResponse.self,
decoder: configuration.decoder
) else {
guard
let error = try? response.decoded(
as: _RawAPIErrorResponse.self,
decoder: configuration.decoder
)
else {
return .api(
message: "Unexpected error",
errorCode: .unexpectedFailure,
Expand All @@ -78,11 +80,14 @@ struct APIClient: Sendable {

let responseAPIVersion = parseResponseAPIVersion(response)

let errorCode: ErrorCode? = if let responseAPIVersion, responseAPIVersion >= apiVersions[._20240101]!.timestamp, let code = error.code {
ErrorCode(code)
} else {
error.errorCode
}
let errorCode: ErrorCode? =
if let responseAPIVersion, responseAPIVersion >= apiVersions[._20240101]!.timestamp,
let code = error.code
{
ErrorCode(code)
} else {
error.errorCode
}

if errorCode == nil, let weakPassword = error.weakPassword {
return .weakPassword(
Expand Down
Loading
Loading