Skip to content
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
48 changes: 48 additions & 0 deletions Sources/Auth/AuthAdmin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,52 @@ public struct AuthAdmin: Sendable {
)
)
}

/// Get a list of users.
///
/// This function should only be called on a server.
///
/// - Warning: Never expose your `service_role` key in the client.
public func listUsers(params: PageParams? = nil) async throws -> ListUsersPaginatedResponse {
struct Response: Decodable {
let users: [User]
let aud: String
}

let httpResponse = try await api.execute(
HTTPRequest(
url: configuration.url.appendingPathComponent("admin/users"),
method: .get,
query: [
URLQueryItem(name: "page", value: params?.page?.description ?? ""),
URLQueryItem(name: "per_page", value: params?.perPage?.description ?? ""),
]
)
)

let response = try httpResponse.decoded(as: Response.self, decoder: configuration.decoder)

var pagination = ListUsersPaginatedResponse(
users: response.users,
aud: response.aud,
lastPage: 0,
total: httpResponse.headers["x-total-count"].flatMap(Int.init) ?? 0
)

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 rel = link.components(separatedBy: ";")[1].components(separatedBy: "=")[1]

if rel == "\"last\"", let lastPage = Int(page) {
pagination.lastPage = lastPage
} else if rel == "\"next\"", let nextPage = Int(page) {
pagination.nextPage = nextPage
}
}
}

return pagination
}
}
24 changes: 22 additions & 2 deletions Sources/Auth/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ public struct User: Codable, Hashable, Identifiable, Sendable {
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
appMetadata = try container.decode([String: AnyJSON].self, forKey: .appMetadata)
userMetadata = try container.decode([String: AnyJSON].self, forKey: .userMetadata)
appMetadata = try container.decodeIfPresent([String: AnyJSON].self, forKey: .appMetadata) ?? [:]
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)
Expand Down Expand Up @@ -816,3 +816,23 @@ public struct OAuthResponse: Codable, Hashable, Sendable {
public let provider: Provider
public let url: URL
}

public struct PageParams {
/// The page number.
public let page: Int?
/// Number of items returned per page.
public let perPage: Int?

public init(page: Int? = nil, perPage: Int? = nil) {
self.page = page
self.perPage = perPage
}
}

public struct ListUsersPaginatedResponse: Hashable, Sendable {
public let users: [User]
public let aud: String
public var nextPage: Int?
public var lastPage: Int
public var total: Int
}
58 changes: 52 additions & 6 deletions Tests/AuthTests/AuthClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,40 @@ final class AuthClientTests: XCTestCase {
XCTAssertEqual(receivedURL.value?.absoluteString, url)
}

func testAdminListUsers() async throws {
let sut = makeSUT { _ in
.stub(
fromFileName: "list-users-response",
headers: [
"X-Total-Count": "669",
"Link": "</admin/users?page=2&per_page=>; rel=\"next\", </admin/users?page=14&per_page=>; rel=\"last\"",
]
)
}

let response = try await sut.admin.listUsers()
XCTAssertEqual(response.total, 669)
XCTAssertEqual(response.nextPage, 2)
XCTAssertEqual(response.lastPage, 14)
}

func testAdminListUsers_noNextPage() async throws {
let sut = makeSUT { _ in
.stub(
fromFileName: "list-users-response",
headers: [
"X-Total-Count": "669",
"Link": "</admin/users?page=14&per_page=>; rel=\"last\"",
]
)
}

let response = try await sut.admin.listUsers()
XCTAssertEqual(response.total, 669)
XCTAssertNil(response.nextPage)
XCTAssertEqual(response.lastPage, 14)
}

private func makeSUT(
fetch: ((URLRequest) async throws -> HTTPResponse)? = nil
) -> AuthClient {
Expand All @@ -331,38 +365,50 @@ final class AuthClientTests: XCTestCase {
}

extension HTTPResponse {
static func stub(_ body: String = "", code: Int = 200) -> HTTPResponse {
static func stub(
_ body: String = "",
code: Int = 200,
headers: [String: String]? = nil
) -> HTTPResponse {
HTTPResponse(
data: body.data(using: .utf8)!,
response: HTTPURLResponse(
url: clientURL,
statusCode: code,
httpVersion: nil,
headerFields: nil
headerFields: headers
)!
)
}

static func stub(fromFileName fileName: String, code: Int = 200) -> HTTPResponse {
static func stub(
fromFileName fileName: String,
code: Int = 200,
headers: [String: String]? = nil
) -> HTTPResponse {
HTTPResponse(
data: json(named: fileName),
response: HTTPURLResponse(
url: clientURL,
statusCode: code,
httpVersion: nil,
headerFields: nil
headerFields: headers
)!
)
}

static func stub(_ value: some Encodable, code: Int = 200) -> HTTPResponse {
static func stub(
_ value: some Encodable,
code: Int = 200,
headers: [String: String]? = nil
) -> HTTPResponse {
HTTPResponse(
data: try! AuthClient.Configuration.jsonEncoder.encode(value),
response: HTTPURLResponse(
url: clientURL,
statusCode: code,
httpVersion: nil,
headerFields: nil
headerFields: headers
)!
)
}
Expand Down
Loading
Loading