diff --git a/CHANGELOG.md b/CHANGELOG.md index 11e68843..be98e016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- [added] Implemented the `GetUserById()` API in the `FirebaseUserManager` class. + - # v1.4.0 diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs index f8ba735a..837ca314 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs @@ -32,7 +32,8 @@ public class FirebaseUserManagerTest [Fact] public void InvalidUidForUserRecord() { - Assert.Throws(() => new UserRecord(null)); + Assert.Throws(() => new UserRecord((string)null)); + Assert.Throws(() => new UserRecord((GetAccountInfoResponse.User)null)); Assert.Throws(() => new UserRecord(string.Empty)); Assert.Throws(() => new UserRecord(new string('a', 129))); } @@ -76,8 +77,16 @@ public async Task GetUserById() { var handler = new MockMessageHandler() { - Response = new UserRecord("user1"), + Response = new GetAccountInfoResponse() + { + Kind = "identitytoolkit#GetAccountInfoResponse", + Users = new List() + { + new GetAccountInfoResponse.User() { UserId = "user1" }, + }, + }, }; + var factory = new MockHttpClientFactory(handler); var userManager = new FirebaseUserManager( new FirebaseUserManagerArgs @@ -114,7 +123,14 @@ public async Task UpdateUser() { var handler = new MockMessageHandler() { - Response = new UserRecord("user1"), + Response = new GetAccountInfoResponse() + { + Kind = "identitytoolkit#GetAccountInfoResponse", + Users = new List() + { + new GetAccountInfoResponse.User() { UserId = "user1" }, + }, + }, }; var factory = new MockHttpClientFactory(handler); var userManager = new FirebaseUserManager( diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs index cd3827ef..a8c436c4 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs @@ -263,6 +263,39 @@ public async Task VerifyIdTokenAsync( .ConfigureAwait(false); } + /// + /// Gets a object containig information about the user who's + /// user ID was specified in . + /// + /// The user ID for the user who's data is to be retrieved. + /// A task that completes with a representing + /// a user with the specified user ID. + /// If user ID argument is null or empty. + /// If a user cannot be found with the specified user ID. + public async Task GetUserAsync(string uid) + { + return await this.GetUserAsync(uid, default(CancellationToken)); + } + + /// + /// Gets a object containig information about the user who's + /// user ID was specified in . + /// + /// The user ID for the user who's data is to be retrieved. + /// A cancellation token to monitor the asynchronous + /// operation. + /// A task that completes with a representing + /// a user with the specified user ID. + /// If user ID argument is null or empty. + /// If a user cannot be found with the specified user ID. + public async Task GetUserAsync( + string uid, CancellationToken cancellationToken) + { + var userManager = this.IfNotDeleted(() => this.userManager.Value); + + return await userManager.GetUserById(uid, cancellationToken); + } + /// /// Sets the specified custom claims on an existing user account. A null claims value /// removes any claims currently set on the user account. The claims must serialize into diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs index 4da678cd..c2b5d78a 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs @@ -81,14 +81,21 @@ public async Task GetUserById( { { "localId", uid }, }; - var response = await this.PostAndDeserializeAsync( + + var response = await this.PostAndDeserializeAsync( getUserPath, payload, cancellationToken).ConfigureAwait(false); - if (response == null || uid != (string)response["localId"]) + if (response == null || response.Users == null || response.Users.Count == 0) + { + throw new FirebaseException($"Failed to get user: {uid}"); + } + + var user = response.Users[0]; + if (user == null || user.UserId != uid) { throw new FirebaseException($"Failed to get user: {uid}"); } - return new UserRecord((string)response["localId"]); + return new UserRecord(user); } /// @@ -102,9 +109,15 @@ public async Task UpdateUserAsync( UserRecord user, CancellationToken cancellationToken = default(CancellationToken)) { const string updatePath = "accounts:update"; - var response = await this.PostAndDeserializeAsync( + var response = await this.PostAndDeserializeAsync( updatePath, user, cancellationToken).ConfigureAwait(false); - if (user.Uid != (string)response["localId"]) + if (response == null || response.Users == null || response.Users.Count == 0) + { + throw new FirebaseException($"Failed to get user: {user.Uid}"); + } + + var updatedUser = response.Users[0]; + if (updatedUser == null || updatedUser.UserId != user.Uid) { throw new FirebaseException($"Failed to update user: {user.Uid}"); } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/IUserInfo.cs b/FirebaseAdmin/FirebaseAdmin/Auth/IUserInfo.cs new file mode 100644 index 00000000..1f6945fa --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Auth/IUserInfo.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace FirebaseAdmin.Auth +{ + /// + /// A collection of standard profile information for a user. Used to expose profile information + /// returned by an identity provider. + /// + public interface IUserInfo + { + /// + /// Gets the user's unique ID assigned by the identity provider. + /// + /// a user ID string. + string Uid + { + get; + } + + /// + /// Gets the user's display name, if available. + /// + /// a display name string or null. + string DisplayName + { + get; + } + + /// + /// Gets the user's email address, if available. + /// + /// an email address string or null. + string Email + { + get; + } + + /// + /// Gets the user's phone number, if available. + /// + /// a phone number string or null. + string PhoneNumber + { + get; + } + + /// + /// Gets the user's photo URL, if available. + /// + /// a URL string or null. + string PhotoUrl + { + get; + } + + /// + /// Gets the ID of the identity provider. This can be a short domain name (e.g. google.com) or + /// the identifier of an OpenID identity provider. + /// + /// an ID string that uniquely identifies the identity provider. + string ProviderId + { + get; + } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/Internal/GetAccountInfoResponse.cs b/FirebaseAdmin/FirebaseAdmin/Auth/Internal/GetAccountInfoResponse.cs new file mode 100644 index 00000000..50daf996 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Auth/Internal/GetAccountInfoResponse.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Auth +{ + /// + /// JSON data binding for GetAccountInfoResponse messages sent by Google identity toolkit service. + /// + internal sealed class GetAccountInfoResponse + { + /// + /// Gets or sets a string representing what kind of account is represented by this object. + /// + [JsonProperty(PropertyName = "kind")] + public string Kind { get; set; } + + /// + /// Gets or sets a list of provider users linked to this account. + /// + [JsonProperty(PropertyName = "users")] + public List Users { get; set; } + + /// + /// JSON data binding for user records. + /// + internal sealed class User + { + /// + /// Gets or sets the user's ID. + /// + [JsonProperty(PropertyName = "localId")] + public string UserId { get; set; } + + /// + /// Gets or sets the user's email address. + /// + [JsonProperty(PropertyName = "email")] + public string Email { get; set; } + + /// + /// Gets or sets the user's phone number. + /// + [JsonProperty(PropertyName = "phoneNumber")] + public string PhoneNumber { get; set; } + + /// + /// Gets or sets a value indicating whether the user's email address is verified or not. + /// + [JsonProperty(PropertyName = "emailVerified")] + public bool EmailVerified { get; set; } + + /// + /// Gets or sets the user's display name. + /// + [JsonProperty(PropertyName = "displayName")] + public string DisplayName { get; set; } + + /// + /// Gets or sets the URL for the user's photo. + /// + [JsonProperty(PropertyName = "photoUrl")] + public string PhotoUrl { get; set; } + + /// + /// Gets or sets a value indicating whether the user is disabled or not. + /// + [JsonProperty(PropertyName = "disabled")] + public bool Disabled { get; set; } + + /// + /// Gets or sets a list of provider-specified data for this user. + /// + [JsonProperty(PropertyName = "providerUserInfo")] + public List Providers { get; set; } + + /// + /// Gets or sets the timestamp representing the time that the user account was created. + /// + [JsonProperty(PropertyName = "createdAt")] + public long CreatedAt { get; set; } + + /// + /// Gets or sets the timestamp representing the last time that the user has logged in. + /// + [JsonProperty(PropertyName = "lastLoginAt")] + public long LastLoginAt { get; set; } + + /// + /// Gets or sets the timestamp representing the time that the user account was first valid. + /// + [JsonProperty(PropertyName = "validSince")] + public long ValidSince { get; set; } + + /// + /// Gets or sets the user's custom claims. + /// + [JsonProperty(PropertyName = "customAttributes")] + public string CustomClaims { get; set; } + } + + /// + /// JSON data binding for provider data. + /// + internal sealed class Provider + { + /// + /// Gets or sets the user's ID. + /// + [JsonProperty(PropertyName = "uid")] + public string UserId { get; set; } + + /// + /// Gets or sets the user's display name. + /// + [JsonProperty(PropertyName = "displayName")] + public string DisplayName { get; set; } + + /// + /// Gets or sets the user's email address. + /// + [JsonProperty(PropertyName = "email")] + public string Email { get; set; } + + /// + /// Gets or sets the user's phone number. + /// + [JsonProperty(PropertyName = "phoneNumber")] + public string PhoneNumber { get; set; } + + /// + /// Gets or sets the URL for the user's photo. + /// + [JsonProperty(PropertyName = "photoUrl")] + public string PhotoUrl { get; set; } + + /// + /// Gets or sets the provider's ID. + /// + [JsonProperty(PropertyName = "providerId")] + public string ProviderID { get; set; } + } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/ProviderUserInfo.cs b/FirebaseAdmin/FirebaseAdmin/Auth/ProviderUserInfo.cs new file mode 100644 index 00000000..1fef1f1d --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Auth/ProviderUserInfo.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace FirebaseAdmin.Auth +{ + /// + /// Contains metadata regarding how a user is known by a particular identity provider (IdP). + /// Instances of this class are immutable and thread safe. + /// + internal sealed class ProviderUserInfo : IUserInfo + { + /// + /// Initializes a new instance of the class with data provided by an authentication provider. + /// + /// The deserialized JSON user data from the provider. + internal ProviderUserInfo(GetAccountInfoResponse.Provider provider) + { + this.Uid = provider.UserId; + this.DisplayName = provider.DisplayName; + this.Email = provider.Email; + this.PhoneNumber = provider.PhoneNumber; + this.PhotoUrl = provider.PhotoUrl; + this.ProviderId = provider.ProviderID; + } + + /// + /// Gets the user's unique ID assigned by the identity provider. + /// + /// a user ID string. + public string Uid { get; private set; } + + /// + /// Gets the user's display name, if available. + /// + /// a display name string or null. + public string DisplayName { get; private set; } + + /// + /// Gets the user's email address, if available. + /// + /// an email address string or null. + public string Email { get; private set; } + + /// + /// Gets the user's phone number. + /// + /// a phone number string or null. + public string PhoneNumber { get; private set; } + + /// + /// Gets the user's photo URL, if available. + /// + /// a URL string or null. + public string PhotoUrl { get; private set; } + + /// + /// Gets the ID of the identity provider. This can be a short domain name (e.g. google.com) or + /// the identifier of an OpenID identity provider. + /// + /// an ID string that uniquely identifies the identity provider. + public string ProviderId { get; private set; } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/UserMetadata.cs b/FirebaseAdmin/FirebaseAdmin/Auth/UserMetadata.cs new file mode 100644 index 00000000..1555c13e --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Auth/UserMetadata.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Auth +{ + /// + /// Contains additional metadata associated with a user account. + /// + public sealed class UserMetadata + { + /// + /// Initializes a new instance of the class with the specified creation and last sign-in timestamps. + /// + /// A timestamp representing the date and time that the user account was created. + /// A timestamp representing the date and time that the user account was last signed-on to. + internal UserMetadata(long creationTimestamp, long lastSignInTimestamp) + { + this.CreationTimestamp = creationTimestamp; + this.LastSignInTimestamp = lastSignInTimestamp; + } + + /// + /// Gets a timestamp representing the date and time that the account was created. + /// + [JsonProperty("creationTimestamp")] + public long CreationTimestamp { get; } + + /// + /// Gets a timestamp representing the last time that the user has logged in. + /// + [JsonProperty("lastSignInTimestamp")] + public long LastSignInTimestamp { get; } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/UserRecord.cs b/FirebaseAdmin/FirebaseAdmin/Auth/UserRecord.cs index ec584e87..9633ae04 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/UserRecord.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/UserRecord.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Text; using Google.Apis.Json; using Newtonsoft.Json; @@ -24,20 +25,82 @@ namespace FirebaseAdmin.Auth /// Contains metadata associated with a Firebase user account. Instances /// of this class are immutable and thread safe. /// - internal class UserRecord + public sealed class UserRecord : IUserInfo { + private const string PROVIDERID = "firebase"; + private string uid; + private string email; + private string phoneNumber; + private bool emailVerified; + private string displayName; + private string photoUrl; + private bool disabled; + private List providers; + private long tokensValidAfterTimestamp; + private UserMetadata userMetaData; private IReadOnlyDictionary customClaims; - public UserRecord(string uid) + /// + /// Initializes a new instance of the class with the specified user ID. + /// + /// The user's ID. + internal UserRecord(string uid) + { + if (string.IsNullOrEmpty(uid) || uid.Length > 128) + { + throw new ArgumentException("User ID must not be null or empty, and be 128 characters or shorter."); + } + + this.uid = uid; + } + + /// + /// Initializes a new instance of the class from an existing instance of the class. + /// + /// The instance to copy the user's data from. + internal UserRecord(GetAccountInfoResponse.User user) { - this.Uid = uid; + if (user == null) + { + throw new ArgumentException("User object must not be null or empty."); + } + else if (string.IsNullOrEmpty(user.UserId)) + { + throw new ArgumentException("User ID must not be null or empty."); + } + + this.uid = user.UserId; + this.email = user.Email; + this.phoneNumber = user.PhoneNumber; + this.emailVerified = user.EmailVerified; + this.displayName = user.DisplayName; + this.photoUrl = user.PhotoUrl; + this.disabled = user.Disabled; + + if (user.Providers == null || user.Providers.Count == 0) + { + this.providers = new List(); + } + else + { + var count = user.Providers.Count; + this.providers = new List(count); + + for (int i = 0; i < count; i++) + { + this.providers.Add(new ProviderUserInfo(user.Providers[i])); + } + } + + this.tokensValidAfterTimestamp = user.ValidSince * 1000; + this.userMetaData = new UserMetadata(user.CreatedAt, user.LastLoginAt); + this.customClaims = UserRecord.ParseCustomClaims(user.CustomClaims); } /// /// Gets the user ID of this user. /// - [JsonProperty("localId")] public string Uid { get => this.uid; @@ -48,6 +111,77 @@ private set } } + /// + /// Gets the user's display name, if available. + /// + /// a display name string or null. + public string DisplayName + { + get => this.displayName; + } + + /// + /// Gets the user's email address, if available. + /// + /// an email address string or null. + public string Email + { + get => this.email; + } + + /// + /// Gets the user's phone number. + /// + /// a phone number string or null. + public string PhoneNumber + { + get => this.phoneNumber; + } + + /// + /// Gets the user's photo URL, if available. + /// + /// a URL string or null. + public string PhotoUrl + { + get => this.photoUrl; + } + + /// + /// Gets the ID of the identity provider. This can be a short domain name (e.g. google.com) or + /// the identifier of an OpenID identity provider. + /// + /// an ID string that uniquely identifies the identity provider. + public string ProviderId + { + get => UserRecord.PROVIDERID; + } + + /// + /// Gets a value indicating whether the user's email address is verified or not. + /// + public bool EmailVerified => this.emailVerified; + + /// + /// Gets a value indicating whether the user account is disabled or not. + /// + public bool Disabled => this.disabled; + + /// + /// Gets a list of provider data for this user. + /// + public IEnumerable Providers => this.providers; + + /// + /// Gets a timestamp representing the date and time that this token will become active. + /// + public long TokensValidAfterTimestamp => this.tokensValidAfterTimestamp; + + /// + /// Gets additional user metadata. + /// + public UserMetadata UserMetaData => this.userMetaData; + /// /// Gets or sets the custom claims set on this user. /// @@ -120,5 +254,17 @@ private static string SerializeClaims(IReadOnlyDictionary claims { return NewtonsoftJsonSerializer.Instance.Serialize(claims); } + + private static IReadOnlyDictionary ParseCustomClaims(string customClaims) + { + if (string.IsNullOrEmpty(customClaims)) + { + return new Dictionary(); + } + else + { + return NewtonsoftJsonSerializer.Instance.Deserialize>(customClaims); + } + } } }