diff --git a/Examples/UserManagement/ProfileView.swift b/Examples/UserManagement/ProfileView.swift index 15771a51..caf70caf 100644 --- a/Examples/UserManagement/ProfileView.swift +++ b/Examples/UserManagement/ProfileView.swift @@ -185,7 +185,7 @@ struct ProfileView: View { do { let currentUserId = try await supabase.auth.session.user.id try await supabase.auth.admin.deleteUser( - id: currentUserId.uuidString, + id: currentUserId, shouldSoftDelete: true ) } catch { diff --git a/Sources/Auth/AuthAdmin.swift b/Sources/Auth/AuthAdmin.swift index 1a31aee8..6aa83841 100644 --- a/Sources/Auth/AuthAdmin.swift +++ b/Sources/Auth/AuthAdmin.swift @@ -6,8 +6,8 @@ // import Foundation -import Helpers import HTTPTypes +import Helpers public struct AuthAdmin: Sendable { let clientID: AuthClientID @@ -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: UUID) 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: UUID, 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 @@ -23,7 +106,7 @@ public struct AuthAdmin: Sendable { /// from the auth schema. /// /// - Warning: Never expose your `service_role` key on the client. - public func deleteUser(id: String, shouldSoftDelete: Bool = false) async throws { + public func deleteUser(id: UUID, shouldSoftDelete: Bool = false) async throws { _ = try await api.execute( HTTPRequest( url: configuration.url.appendingPathComponent("admin/users/\(id)"), @@ -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) { @@ -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 { diff --git a/Sources/Auth/Deprecated.swift b/Sources/Auth/Deprecated.swift index 3f1eba1d..5a77e208 100644 --- a/Sources/Auth/Deprecated.swift +++ b/Sources/Auth/Deprecated.swift @@ -32,7 +32,8 @@ extension JSONEncoder { *, deprecated, renamed: "AuthClient.Configuration.jsonEncoder", - message: "Access to the default JSONEncoder instance moved to AuthClient.Configuration.jsonEncoder" + message: + "Access to the default JSONEncoder instance moved to AuthClient.Configuration.jsonEncoder" ) public static var goTrue: JSONEncoder { AuthClient.Configuration.jsonEncoder @@ -44,7 +45,8 @@ extension JSONDecoder { *, deprecated, renamed: "AuthClient.Configuration.jsonDecoder", - message: "Access to the default JSONDecoder instance moved to AuthClient.Configuration.jsonDecoder" + message: + "Access to the default JSONDecoder instance moved to AuthClient.Configuration.jsonDecoder" ) public static var goTrue: JSONDecoder { AuthClient.Configuration.jsonDecoder @@ -65,7 +67,8 @@ extension AuthClient.Configuration { @available( *, deprecated, - message: "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)" + message: + "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)" ) public init( url: URL, @@ -103,7 +106,8 @@ extension AuthClient { @available( *, deprecated, - message: "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)" + message: + "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)" ) public init( url: URL, @@ -129,3 +133,18 @@ extension AuthClient { @available(*, deprecated, message: "Use MFATotpEnrollParams or MFAPhoneEnrollParams instead.") public typealias MFAEnrollParams = MFATotpEnrollParams + +extension AuthAdmin { + @available( + *, + deprecated, + message: "Use deleteUser with UUID instead of string." + ) + public func deleteUser(id: String, shouldSoftDelete: Bool = false) async throws { + guard let id = UUID(uuidString: id) else { + fatalError("id should be a valid UUID") + } + + try await self.deleteUser(id: id, shouldSoftDelete: shouldSoftDelete) + } +} diff --git a/Sources/Auth/Internal/APIClient.swift b/Sources/Auth/Internal/APIClient.swift index fc2d8521..af7eece5 100644 --- a/Sources/Auth/Internal/APIClient.swift +++ b/Sources/Auth/Internal/APIClient.swift @@ -1,6 +1,6 @@ import Foundation -import Helpers import HTTPTypes +import Helpers extension HTTPClient { init(configuration: AuthClient.Configuration) { @@ -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. ) ) ) @@ -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) } @@ -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, @@ -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( diff --git a/Sources/Auth/Types.swift b/Sources/Auth/Types.swift index 6e51684b..a14f2e68 100644 --- a/Sources/Auth/Types.swift +++ b/Sources/Auth/Types.swift @@ -195,7 +195,8 @@ public struct User: Codable, Hashable, Identifiable, Sendable { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(UUID.self, forKey: .id) appMetadata = try container.decodeIfPresent([String: AnyJSON].self, forKey: .appMetadata) ?? [:] - userMetadata = try container.decodeIfPresent([String: AnyJSON].self, forKey: .userMetadata) ?? [:] + userMetadata = + try container.decodeIfPresent([String: AnyJSON].self, forKey: .userMetadata) ?? [:] aud = try container.decode(String.self, forKey: .aud) confirmationSentAt = try container.decodeIfPresent(Date.self, forKey: .confirmationSentAt) recoverySentAt = try container.decodeIfPresent(Date.self, forKey: .recoverySentAt) @@ -248,22 +249,12 @@ public struct UserIdentity: Codable, Hashable, Identifiable, Sendable { self.updatedAt = updatedAt } - private enum CodingKeys: CodingKey { - case id - case identityId - case userId - case identityData - case provider - case createdAt - case lastSignInAt - case updatedAt - } - public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) - identityId = try container.decodeIfPresent(UUID.self, forKey: .identityId) + identityId = + try container.decodeIfPresent(UUID.self, forKey: .identityId) ?? UUID(uuidString: "00000000-0000-0000-0000-000000000000")! userId = try container.decode(UUID.self, forKey: .userId) identityData = try container.decodeIfPresent([String: AnyJSON].self, forKey: .identityData) @@ -507,6 +498,73 @@ public struct UserAttributes: Codable, Hashable, Sendable { } } +public struct AdminUserAttributes: Encodable, Hashable, Sendable { + + /// A custom data object to store the user's application specific metadata. This maps to the `auth.users.app_metadata` column. + public var appMetadata: [String: AnyJSON]? + + /// Determines how long a user is banned for. + public var banDuration: String? + + /// The user's email. + public var email: String? + + /// Confirms the user's email address if set to true. + public var emailConfirm: Bool? + + /// The `id` for the user. + public var id: String? + + /// The nonce sent for reauthentication if the user's password is to be updated. + public var nonce: String? + + /// The user's password. + public var password: String? + + /// The `password_hash` for the user's password. + public var passwordHash: String? + + /// The user's phone. + public var phone: String? + + /// Confirms the user's phone number if set to true. + public var phoneConfirm: Bool? + + /// The role claim set in the user's access token JWT. + public var role: String? + + /// A custom data object to store the user's metadata. This maps to the `auth.users.raw_user_meta_data` column. + public var userMetadata: [String: AnyJSON]? + + public init( + appMetadata: [String: AnyJSON]? = nil, + banDuration: String? = nil, + email: String? = nil, + emailConfirm: Bool? = nil, + id: String? = nil, + nonce: String? = nil, + password: String? = nil, + passwordHash: String? = nil, + phone: String? = nil, + phoneConfirm: Bool? = nil, + role: String? = nil, + userMetadata: [String: AnyJSON]? = nil + ) { + self.appMetadata = appMetadata + self.banDuration = banDuration + self.email = email + self.emailConfirm = emailConfirm + self.id = id + self.nonce = nonce + self.password = password + self.passwordHash = passwordHash + self.phone = phone + self.phoneConfirm = phoneConfirm + self.role = role + self.userMetadata = userMetadata + } +} + struct RecoverParams: Codable, Hashable, Sendable { var email: String var gotrueMetaSecurity: AuthMetaSecurity? @@ -719,8 +777,8 @@ public struct AMREntry: Decodable, Hashable, Sendable { extension AMREntry { init?(value: Any) { guard let dict = value as? [String: Any], - let method = dict["method"] as? Method, - let timestamp = dict["timestamp"] as? TimeInterval + let method = dict["method"] as? Method, + let timestamp = dict["timestamp"] as? TimeInterval else { return nil } @@ -839,3 +897,119 @@ public struct ListUsersPaginatedResponse: Hashable, Sendable { public var lastPage: Int public var total: Int } + +public struct GenerateLinkParams: Sendable { + struct Body: Encodable { + var type: GenerateLinkType + var email: String + var password: String? + var newEmail: String? + var data: [String: AnyJSON]? + } + var body: Body + var redirectTo: URL? + + /// Generates a signup link. + public static func signUp( + email: String, + password: String, + data: [String: AnyJSON]? = nil, + redirectTo: URL? = nil + ) -> GenerateLinkParams { + GenerateLinkParams( + body: .init( + type: .signup, + email: email, + password: password, + data: data + ), + redirectTo: redirectTo + ) + } + + /// Generates an invite link. + public static func invite( + email: String, + data: [String: AnyJSON]? = nil, + redirectTo: URL? = nil + ) -> GenerateLinkParams { + GenerateLinkParams( + body: .init( + type: .invite, + email: email, + data: data + ), + redirectTo: redirectTo + ) + } + + /// Generates a magic link. + public static func magicLink( + email: String, + data: [String: AnyJSON]? = nil, + redirectTo: URL? = nil + ) -> GenerateLinkParams { + GenerateLinkParams( + body: .init( + type: .magiclink, + email: email, + data: data + ), + redirectTo: redirectTo + ) + } + + /// Generates a recovery link. + public static func recovery( + email: String, + redirectTo: URL? = nil + ) -> GenerateLinkParams { + GenerateLinkParams( + body: .init( + type: .recovery, + email: email + ), + redirectTo: redirectTo + ) + } + +} + +/// The response from the `generateLink` function. +public struct GenerateLinkResponse: Hashable, Sendable { + /// The properties related to the email link generated. + public let properties: GenerateLinkProperties + /// The user that the email link is associated to. + public let user: User +} + +/// The properties related to the email link generated. +public struct GenerateLinkProperties: Decodable, Hashable, Sendable { + /// The email link to send to the users. + /// The action link follows the following format: auth/v1/verify?type={verification_type}&token={hashed_token}&redirect_to={redirect_to} + public let actionLink: URL + /// The raw ramil OTP. + /// You should send this in the email if you want your users to verify using an OTP instead of the action link. + public let emailOTP: String + /// The hashed token appended to the action link. + public let hashedToken: String + /// The URL appended to the action link. + public let redirectTo: URL + /// The verification type that the emaillink is associated to. + public let verificationType: GenerateLinkType +} + +public struct GenerateLinkType: RawRepresentable, Codable, Hashable, Sendable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public static let signup = GenerateLinkType(rawValue: "signup") + public static let invite = GenerateLinkType(rawValue: "invite") + public static let magiclink = GenerateLinkType(rawValue: "magiclink") + public static let recovery = GenerateLinkType(rawValue: "recovery") + public static let emailChangeCurrent = GenerateLinkType(rawValue: "email_change_current") + public static let emailChangeNew = GenerateLinkType(rawValue: "email_change_new") +} diff --git a/Tests/AuthTests/AuthClientTests.swift b/Tests/AuthTests/AuthClientTests.swift index 259ee839..cc57b70b 100644 --- a/Tests/AuthTests/AuthClientTests.swift +++ b/Tests/AuthTests/AuthClientTests.swift @@ -1330,7 +1330,7 @@ final class AuthClientTests: XCTestCase { } func testDeleteUser() async throws { - let id = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + let id = UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")! Mock( url: clientURL.appendingPathComponent("admin/users/\(id)"), @@ -1940,6 +1940,167 @@ final class AuthClientTests: XCTestCase { ) } + func testgetUserById() async throws { + let id = UUID(uuidString: "859f402d-b3de-4105-a1b9-932836d9193b")! + let sut = makeSUT() + + Mock( + url: clientURL.appendingPathComponent("admin/users/\(id)"), + statusCode: 200, + data: [.get: MockData.user] + ) + .snapshotRequest { + #""" + curl \ + --header "X-Client-Info: auth-swift/0.0.0" \ + --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \ + "http://localhost:54321/auth/v1/admin/users/859f402d-b3de-4105-a1b9-932836d9193b" + """# + } + .register() + + let user = try await sut.admin.getUserById(id) + + expectNoDifference(user.id, id) + } + + func testUpdateUserById() async throws { + let id = UUID(uuidString:"859f402d-b3de-4105-a1b9-932836d9193b")! + let sut = makeSUT() + + Mock( + url: clientURL.appendingPathComponent("admin/users/\(id)"), + statusCode: 200, + data: [.put: MockData.user] + ) + .snapshotRequest { + #""" + curl \ + --request PUT \ + --header "Content-Length: 63" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: auth-swift/0.0.0" \ + --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \ + --data "{\"phone\":\"1234567890\",\"user_metadata\":{\"full_name\":\"John Doe\"}}" \ + "http://localhost:54321/auth/v1/admin/users/859f402d-b3de-4105-a1b9-932836d9193b" + """# + } + .register() + + let attributes = AdminUserAttributes( + phone: "1234567890", + userMetadata: [ + "full_name": "John Doe" + ] + ) + + let user = try await sut.admin.updateUserById(id, attributes: attributes) + + expectNoDifference(user.id, id) + } + + func testCreateUser() async throws { + let sut = makeSUT() + + Mock( + url: clientURL.appendingPathComponent("admin/users"), + statusCode: 200, + data: [.post: MockData.user] + ) + .snapshotRequest { + #""" + curl \ + --request POST \ + --header "Content-Length: 98" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: auth-swift/0.0.0" \ + --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \ + --data "{\"email\":\"test@example.com\",\"password\":\"password\",\"password_hash\":\"password\",\"phone\":\"1234567890\"}" \ + "http://localhost:54321/auth/v1/admin/users" + """# + } + .register() + + let attributes = AdminUserAttributes( + email: "test@example.com", + password: "password", + passwordHash: "password", + phone: "1234567890" + ) + + _ = try await sut.admin.createUser(attributes: attributes) + } + + func testGenerateLink_signUp() async throws { + let sut = makeSUT() + + Mock( + url: clientURL.appendingPathComponent("admin/generate_link"), + statusCode: 200, + data: [ + .post: try! AuthClient.Configuration.jsonEncoder.encode([ + "properties": [ + "action_link": + "https://example.com/auth/v1/verify?type=signup&token={hashed_token}&redirect_to=https://example.com", + "email_otp": "123456", + "hashed_token": "hashed_token", + "redirect_to": "https://example.com", + "verification_type": "signup", + ], + "user": AnyJSON(User(fromMockNamed: "user")), + ]) + ] + ) + .register() + + let link = try await sut.admin.generateLink( + params: .signUp( + email: "test@example.com", + password: "password", + data: ["full_name": "John Doe"] + ) + ) + + expectNoDifference( + link.properties.actionLink.absoluteString, + "https://example.com/auth/v1/verify?type=signup&token={hashed_token}&redirect_to=https://example.com" + ) + } + + func testInviteUserByEmail() async throws { + let sut = makeSUT() + + Mock( + url: clientURL.appendingPathComponent("admin/invite"), + ignoreQuery: true, + statusCode: 200, + data: [.post: MockData.user] + ) + .snapshotRequest { + #""" + curl \ + --request POST \ + --header "Content-Length: 60" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: auth-swift/0.0.0" \ + --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \ + --data "{\"data\":{\"full_name\":\"John Doe\"},\"email\":\"test@example.com\"}" \ + "http://localhost:54321/auth/v1/admin/invite?redirect_to=https://example.com" + """# + } + .register() + + _ = try await sut.admin.inviteUserByEmail( + "test@example.com", + data: ["full_name": "John Doe"], + redirectTo: URL(string: "https://example.com") + ) + } + private func makeSUT(flowType: AuthFlowType = .pkce) -> AuthClient { let sessionConfiguration = URLSessionConfiguration.default sessionConfiguration.protocolClasses = [MockingURLProtocol.self] diff --git a/Tests/AuthTests/RequestsTests.swift b/Tests/AuthTests/RequestsTests.swift index b81b71bc..0db09eb2 100644 --- a/Tests/AuthTests/RequestsTests.swift +++ b/Tests/AuthTests/RequestsTests.swift @@ -338,7 +338,7 @@ final class RequestsTests: XCTestCase { func testDeleteUser() async { let sut = makeSUT() - let id = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + let id = UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")! await assert { try await sut.admin.deleteUser(id: id) } diff --git a/Tests/IntegrationTests/.vscode/extensions.json b/Tests/IntegrationTests/.vscode/extensions.json index 74baffcc..09cf720d 100644 --- a/Tests/IntegrationTests/.vscode/extensions.json +++ b/Tests/IntegrationTests/.vscode/extensions.json @@ -1,3 +1,5 @@ { - "recommendations": ["denoland.vscode-deno"] + "recommendations": [ + "denoland.vscode-deno" + ] } diff --git a/Tests/IntegrationTests/.vscode/settings.json b/Tests/IntegrationTests/.vscode/settings.json index af62c23f..35b884cd 100644 --- a/Tests/IntegrationTests/.vscode/settings.json +++ b/Tests/IntegrationTests/.vscode/settings.json @@ -1,4 +1,7 @@ { + "[typescript]": { + "editor.defaultFormatter": "denoland.vscode-deno" + }, "deno.enablePaths": [ "supabase/functions" ], @@ -17,8 +20,5 @@ "fs", "http", "net" - ], - "[typescript]": { - "editor.defaultFormatter": "denoland.vscode-deno" - } + ] } diff --git a/Tests/IntegrationTests/AuthClientIntegrationTests.swift b/Tests/IntegrationTests/AuthClientIntegrationTests.swift index ad62d350..d2a55046 100644 --- a/Tests/IntegrationTests/AuthClientIntegrationTests.swift +++ b/Tests/IntegrationTests/AuthClientIntegrationTests.swift @@ -172,7 +172,8 @@ final class AuthClientIntegrationTests: XCTestCase { func testUserIdentities() async throws { let session = try await signUpIfNeededOrSignIn(email: mockEmail(), password: mockPassword()) let identities = try await authClient.userIdentities() - expectNoDifference(session.user.identities?.map(\.identityId) ?? [], identities.map(\.identityId)) + expectNoDifference( + session.user.identities?.map(\.identityId) ?? [], identities.map(\.identityId)) } func testUnlinkIdentity_withOnlyOneIdentity() async throws { @@ -273,6 +274,54 @@ final class AuthClientIntegrationTests: XCTestCase { } } + func testGenerateLink_signUp() async throws { + let client = Self.makeClient(serviceRole: true) + let email = mockEmail() + let password = mockPassword() + + let link = try await client.admin.generateLink( + params: .signUp( + email: email, + password: password, + data: ["full_name": "John Doe"] + ) + ) + + expectNoDifference(link.properties.actionLink.path, "/auth/v1/verify") + expectNoDifference(link.properties.verificationType, .signup) + expectNoDifference(link.user.email, email) + } + + func testGenerateLink_magicLink() async throws { + let client = Self.makeClient(serviceRole: true) + let email = mockEmail() + + let link = try await client.admin.generateLink(params: .magicLink(email: email)) + + expectNoDifference(link.properties.verificationType, .magiclink) + } + + // func testGenerateLink_recovery() async throws { + // let client = Self.makeClient(serviceRole: true) + // let email = mockEmail() + // let password = mockPassword() + + // _ = try await client.signUp(email: email, password: password) + + // let link = try await client.admin.generateLink(params: .recovery(email: email)) + + // expectNoDifference(link.properties.verificationType, .recovery) + // } + + func testGenerateLink_invite() async throws { + let client = Self.makeClient(serviceRole: true) + let email = mockEmail() + + let link = try await client.admin.generateLink(params: .invite(email: email)) + + expectNoDifference(link.properties.verificationType, .invite) + } + @discardableResult private func signUpIfNeededOrSignIn( email: String, diff --git a/Tests/IntegrationTests/DotEnv.swift b/Tests/IntegrationTests/DotEnv.swift index c7b179a5..678b89b3 100644 --- a/Tests/IntegrationTests/DotEnv.swift +++ b/Tests/IntegrationTests/DotEnv.swift @@ -1,7 +1,7 @@ enum DotEnv { static let SUPABASE_URL = "http://localhost:54321" static let SUPABASE_ANON_KEY = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91dGxvb2stZGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTM3MzYwMjgsImV4cCI6MjAyOTMxMjAyOH0.6Y900000000000000000000000000000000000000000000000000000000000000" + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" static let SUPABASE_SERVICE_ROLE_KEY = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91dGxvb2stZGV2Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTcxMzczNjAyOCwiZXhwIjoyMDI5MzEyMDI4fQ.0000000000000000000000000000000000000000000000000000000000000000" + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU" }