Skip to content

Commit 1851262

Browse files
authored
feat(auth): add listUsers admin method (#539)
1 parent 95e249f commit 1851262

File tree

5 files changed

+1385
-11
lines changed

5 files changed

+1385
-11
lines changed

Sources/Auth/AuthAdmin.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,52 @@ public struct AuthAdmin: Sendable {
3333
)
3434
)
3535
}
36+
37+
/// Get a list of users.
38+
///
39+
/// This function should only be called on a server.
40+
///
41+
/// - Warning: Never expose your `service_role` key in the client.
42+
public func listUsers(params: PageParams? = nil) async throws -> ListUsersPaginatedResponse {
43+
struct Response: Decodable {
44+
let users: [User]
45+
let aud: String
46+
}
47+
48+
let httpResponse = try await api.execute(
49+
HTTPRequest(
50+
url: configuration.url.appendingPathComponent("admin/users"),
51+
method: .get,
52+
query: [
53+
URLQueryItem(name: "page", value: params?.page?.description ?? ""),
54+
URLQueryItem(name: "per_page", value: params?.perPage?.description ?? ""),
55+
]
56+
)
57+
)
58+
59+
let response = try httpResponse.decoded(as: Response.self, decoder: configuration.decoder)
60+
61+
var pagination = ListUsersPaginatedResponse(
62+
users: response.users,
63+
aud: response.aud,
64+
lastPage: 0,
65+
total: httpResponse.headers["x-total-count"].flatMap(Int.init) ?? 0
66+
)
67+
68+
let links = httpResponse.headers["link"]?.components(separatedBy: ",") ?? []
69+
if !links.isEmpty {
70+
for link in links {
71+
let page = link.components(separatedBy: ";")[0].components(separatedBy: "=")[1].prefix(while: \.isNumber)
72+
let rel = link.components(separatedBy: ";")[1].components(separatedBy: "=")[1]
73+
74+
if rel == "\"last\"", let lastPage = Int(page) {
75+
pagination.lastPage = lastPage
76+
} else if rel == "\"next\"", let nextPage = Int(page) {
77+
pagination.nextPage = nextPage
78+
}
79+
}
80+
}
81+
82+
return pagination
83+
}
3684
}

Sources/Auth/Types.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ public struct User: Codable, Hashable, Identifiable, Sendable {
193193
public init(from decoder: any Decoder) throws {
194194
let container = try decoder.container(keyedBy: CodingKeys.self)
195195
id = try container.decode(UUID.self, forKey: .id)
196-
appMetadata = try container.decode([String: AnyJSON].self, forKey: .appMetadata)
197-
userMetadata = try container.decode([String: AnyJSON].self, forKey: .userMetadata)
196+
appMetadata = try container.decodeIfPresent([String: AnyJSON].self, forKey: .appMetadata) ?? [:]
197+
userMetadata = try container.decodeIfPresent([String: AnyJSON].self, forKey: .userMetadata) ?? [:]
198198
aud = try container.decode(String.self, forKey: .aud)
199199
confirmationSentAt = try container.decodeIfPresent(Date.self, forKey: .confirmationSentAt)
200200
recoverySentAt = try container.decodeIfPresent(Date.self, forKey: .recoverySentAt)
@@ -816,3 +816,23 @@ public struct OAuthResponse: Codable, Hashable, Sendable {
816816
public let provider: Provider
817817
public let url: URL
818818
}
819+
820+
public struct PageParams {
821+
/// The page number.
822+
public let page: Int?
823+
/// Number of items returned per page.
824+
public let perPage: Int?
825+
826+
public init(page: Int? = nil, perPage: Int? = nil) {
827+
self.page = page
828+
self.perPage = perPage
829+
}
830+
}
831+
832+
public struct ListUsersPaginatedResponse: Hashable, Sendable {
833+
public let users: [User]
834+
public let aud: String
835+
public var nextPage: Int?
836+
public var lastPage: Int
837+
public var total: Int
838+
}

Tests/AuthTests/AuthClientTests.swift

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,40 @@ final class AuthClientTests: XCTestCase {
306306
XCTAssertEqual(receivedURL.value?.absoluteString, url)
307307
}
308308

309+
func testAdminListUsers() async throws {
310+
let sut = makeSUT { _ in
311+
.stub(
312+
fromFileName: "list-users-response",
313+
headers: [
314+
"X-Total-Count": "669",
315+
"Link": "</admin/users?page=2&per_page=>; rel=\"next\", </admin/users?page=14&per_page=>; rel=\"last\"",
316+
]
317+
)
318+
}
319+
320+
let response = try await sut.admin.listUsers()
321+
XCTAssertEqual(response.total, 669)
322+
XCTAssertEqual(response.nextPage, 2)
323+
XCTAssertEqual(response.lastPage, 14)
324+
}
325+
326+
func testAdminListUsers_noNextPage() async throws {
327+
let sut = makeSUT { _ in
328+
.stub(
329+
fromFileName: "list-users-response",
330+
headers: [
331+
"X-Total-Count": "669",
332+
"Link": "</admin/users?page=14&per_page=>; rel=\"last\"",
333+
]
334+
)
335+
}
336+
337+
let response = try await sut.admin.listUsers()
338+
XCTAssertEqual(response.total, 669)
339+
XCTAssertNil(response.nextPage)
340+
XCTAssertEqual(response.lastPage, 14)
341+
}
342+
309343
private func makeSUT(
310344
fetch: ((URLRequest) async throws -> HTTPResponse)? = nil
311345
) -> AuthClient {
@@ -331,38 +365,50 @@ final class AuthClientTests: XCTestCase {
331365
}
332366

333367
extension HTTPResponse {
334-
static func stub(_ body: String = "", code: Int = 200) -> HTTPResponse {
368+
static func stub(
369+
_ body: String = "",
370+
code: Int = 200,
371+
headers: [String: String]? = nil
372+
) -> HTTPResponse {
335373
HTTPResponse(
336374
data: body.data(using: .utf8)!,
337375
response: HTTPURLResponse(
338376
url: clientURL,
339377
statusCode: code,
340378
httpVersion: nil,
341-
headerFields: nil
379+
headerFields: headers
342380
)!
343381
)
344382
}
345383

346-
static func stub(fromFileName fileName: String, code: Int = 200) -> HTTPResponse {
384+
static func stub(
385+
fromFileName fileName: String,
386+
code: Int = 200,
387+
headers: [String: String]? = nil
388+
) -> HTTPResponse {
347389
HTTPResponse(
348390
data: json(named: fileName),
349391
response: HTTPURLResponse(
350392
url: clientURL,
351393
statusCode: code,
352394
httpVersion: nil,
353-
headerFields: nil
395+
headerFields: headers
354396
)!
355397
)
356398
}
357399

358-
static func stub(_ value: some Encodable, code: Int = 200) -> HTTPResponse {
400+
static func stub(
401+
_ value: some Encodable,
402+
code: Int = 200,
403+
headers: [String: String]? = nil
404+
) -> HTTPResponse {
359405
HTTPResponse(
360406
data: try! AuthClient.Configuration.jsonEncoder.encode(value),
361407
response: HTTPURLResponse(
362408
url: clientURL,
363409
statusCode: code,
364410
httpVersion: nil,
365-
headerFields: nil
411+
headerFields: headers
366412
)!
367413
)
368414
}

0 commit comments

Comments
 (0)