Allow the "get profile" endpoint to include a PNI credential
This commit is contained in:
parent
93a7c60a15
commit
68412b3901
|
@ -5,8 +5,12 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||||
|
|
||||||
import com.codahale.metrics.annotation.Timed;
|
import com.codahale.metrics.annotation.Timed;
|
||||||
import io.dropwizard.auth.Auth;
|
import io.dropwizard.auth.Auth;
|
||||||
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
import io.micrometer.core.instrument.Tags;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
@ -24,7 +28,9 @@ import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.validation.valueextraction.Unwrapping;
|
import javax.validation.valueextraction.Unwrapping;
|
||||||
|
import javax.ws.rs.BadRequestException;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.DefaultValue;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.HeaderParam;
|
import javax.ws.rs.HeaderParam;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
|
@ -39,14 +45,13 @@ import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
|
||||||
import io.micrometer.core.instrument.Tags;
|
|
||||||
import org.apache.commons.codec.DecoderException;
|
import org.apache.commons.codec.DecoderException;
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
import org.apache.commons.codec.binary.Hex;
|
import org.apache.commons.codec.binary.Hex;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.signal.zkgroup.InvalidInputException;
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
import org.signal.zkgroup.VerificationFailedException;
|
import org.signal.zkgroup.VerificationFailedException;
|
||||||
|
import org.signal.zkgroup.profiles.PniCredentialResponse;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
|
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||||
|
@ -80,8 +85,6 @@ import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||||
|
|
||||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
|
||||||
|
|
||||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
@Path("/v1/profile")
|
@Path("/v1/profile")
|
||||||
public class ProfileController {
|
public class ProfileController {
|
||||||
|
@ -103,6 +106,9 @@ public class ProfileController {
|
||||||
private final S3Client s3client;
|
private final S3Client s3client;
|
||||||
private final String bucket;
|
private final String bucket;
|
||||||
|
|
||||||
|
private static final String PROFILE_KEY_CREDENTIAL_TYPE = "profileKey";
|
||||||
|
private static final String PNI_CREDENTIAL_TYPE = "pni";
|
||||||
|
|
||||||
private static final String LEGACY_GET_PROFILE_COUNTER_NAME = name(ProfileController.class, "legacyGetProfileByPlatform");
|
private static final String LEGACY_GET_PROFILE_COUNTER_NAME = name(ProfileController.class, "legacyGetProfileByPlatform");
|
||||||
|
|
||||||
public ProfileController(
|
public ProfileController(
|
||||||
|
@ -205,7 +211,7 @@ public class ProfileController {
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/{uuid}/{version}")
|
@Path("/{uuid}/{version}")
|
||||||
public Optional<Profile> getProfile(
|
public Profile getProfile(
|
||||||
@Auth Optional<AuthenticatedAccount> auth,
|
@Auth Optional<AuthenticatedAccount> auth,
|
||||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||||
@Context ContainerRequestContext containerRequestContext,
|
@Context ContainerRequestContext containerRequestContext,
|
||||||
|
@ -214,87 +220,107 @@ public class ProfileController {
|
||||||
throws RateLimitExceededException {
|
throws RateLimitExceededException {
|
||||||
return getVersionedProfile(auth.map(AuthenticatedAccount::getAccount), accessKey,
|
return getVersionedProfile(auth.map(AuthenticatedAccount::getAccount), accessKey,
|
||||||
getAcceptableLanguagesForRequest(containerRequestContext), uuid,
|
getAcceptableLanguagesForRequest(containerRequestContext), uuid,
|
||||||
version, Optional.empty());
|
version, Optional.empty(), Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/{uuid}/{version}/{credentialRequest}")
|
@Path("/{uuid}/{version}/{credentialRequest}")
|
||||||
public Optional<Profile> getProfile(
|
public Profile getProfile(
|
||||||
@Auth Optional<AuthenticatedAccount> auth,
|
@Auth Optional<AuthenticatedAccount> auth,
|
||||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||||
@Context ContainerRequestContext containerRequestContext,
|
@Context ContainerRequestContext containerRequestContext,
|
||||||
@PathParam("uuid") UUID uuid,
|
@PathParam("uuid") UUID uuid,
|
||||||
@PathParam("version") String version,
|
@PathParam("version") String version,
|
||||||
@PathParam("credentialRequest") String credentialRequest)
|
@PathParam("credentialRequest") String credentialRequest,
|
||||||
|
@QueryParam("credentialType") @DefaultValue(PROFILE_KEY_CREDENTIAL_TYPE) String credentialType)
|
||||||
throws RateLimitExceededException {
|
throws RateLimitExceededException {
|
||||||
return getVersionedProfile(auth.map(AuthenticatedAccount::getAccount), accessKey,
|
return getVersionedProfile(auth.map(AuthenticatedAccount::getAccount), accessKey,
|
||||||
getAcceptableLanguagesForRequest(containerRequestContext), uuid,
|
getAcceptableLanguagesForRequest(containerRequestContext), uuid,
|
||||||
version, Optional.of(credentialRequest));
|
version, Optional.of(credentialRequest), Optional.of(credentialType));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Profile> getVersionedProfile(
|
private Profile getVersionedProfile(
|
||||||
Optional<Account> requestAccount,
|
Optional<Account> requestAccount,
|
||||||
Optional<Anonymous> accessKey,
|
Optional<Anonymous> accessKey,
|
||||||
List<Locale> acceptableLanguages,
|
List<Locale> acceptableLanguages,
|
||||||
UUID uuid,
|
UUID uuid,
|
||||||
String version,
|
String version,
|
||||||
Optional<String> credentialRequest)
|
Optional<String> credentialRequest,
|
||||||
|
Optional<String> credentialType)
|
||||||
throws RateLimitExceededException {
|
throws RateLimitExceededException {
|
||||||
try {
|
if (requestAccount.isEmpty() && accessKey.isEmpty()) {
|
||||||
if (requestAccount.isEmpty() && accessKey.isEmpty()) {
|
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
boolean isSelf = false;
|
boolean isSelf = false;
|
||||||
if (requestAccount.isPresent()) {
|
if (requestAccount.isPresent()) {
|
||||||
UUID authedUuid = requestAccount.get().getUuid();
|
UUID authedUuid = requestAccount.get().getUuid();
|
||||||
rateLimiters.getProfileLimiter().validate(authedUuid);
|
rateLimiters.getProfileLimiter().validate(authedUuid);
|
||||||
isSelf = uuid.equals(authedUuid);
|
isSelf = uuid.equals(authedUuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid);
|
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid);
|
||||||
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
|
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
|
||||||
|
|
||||||
assert(accountProfile.isPresent());
|
assert(accountProfile.isPresent());
|
||||||
|
|
||||||
Optional<String> username = accountProfile.flatMap(Account::getUsername);
|
Optional<String> username = accountProfile.flatMap(Account::getUsername);
|
||||||
Optional<VersionedProfile> profile = profilesManager.get(uuid, version);
|
Optional<VersionedProfile> profile = profilesManager.get(uuid, version);
|
||||||
|
|
||||||
String name = profile.map(VersionedProfile::getName).orElse(accountProfile.get().getProfileName());
|
String name = profile.map(VersionedProfile::getName).orElse(accountProfile.get().getProfileName());
|
||||||
String about = profile.map(VersionedProfile::getAbout).orElse(null);
|
String about = profile.map(VersionedProfile::getAbout).orElse(null);
|
||||||
String aboutEmoji = profile.map(VersionedProfile::getAboutEmoji).orElse(null);
|
String aboutEmoji = profile.map(VersionedProfile::getAboutEmoji).orElse(null);
|
||||||
String avatar = profile.map(VersionedProfile::getAvatar).orElse(accountProfile.get().getAvatar());
|
String avatar = profile.map(VersionedProfile::getAvatar).orElse(accountProfile.get().getAvatar());
|
||||||
Optional<String> currentProfileVersion = accountProfile.get().getCurrentProfileVersion();
|
Optional<String> currentProfileVersion = accountProfile.get().getCurrentProfileVersion();
|
||||||
|
|
||||||
// Allow requests where either the version matches the latest version on Account or the latest version on Account
|
// Allow requests where either the version matches the latest version on Account or the latest version on Account
|
||||||
// is empty to read the payment address.
|
// is empty to read the payment address.
|
||||||
final String paymentAddress = profile
|
final String paymentAddress = profile
|
||||||
.filter(p -> currentProfileVersion.map(v -> v.equals(version)).orElse(true))
|
.filter(p -> currentProfileVersion.map(v -> v.equals(version)).orElse(true))
|
||||||
.map(VersionedProfile::getPaymentAddress)
|
.map(VersionedProfile::getPaymentAddress)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
|
||||||
Optional<ProfileKeyCredentialResponse> credential = getProfileCredential(credentialRequest, profile, uuid);
|
final ProfileKeyCredentialResponse profileKeyCredentialResponse;
|
||||||
|
final PniCredentialResponse pniCredentialResponse;
|
||||||
|
|
||||||
return Optional.of(new Profile(
|
if (credentialRequest.isPresent() && credentialType.isPresent() && profile.isPresent() && requestAccount.isPresent()) {
|
||||||
name,
|
if (PNI_CREDENTIAL_TYPE.equals(credentialType.get())) {
|
||||||
about,
|
profileKeyCredentialResponse = null;
|
||||||
aboutEmoji,
|
pniCredentialResponse = getPniCredential(credentialRequest.get(),
|
||||||
avatar,
|
profile.get(),
|
||||||
paymentAddress,
|
requestAccount.get().getUuid(),
|
||||||
accountProfile.get().getIdentityKey(),
|
requestAccount.get().getPhoneNumberIdentifier());
|
||||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
} else if (PROFILE_KEY_CREDENTIAL_TYPE.equals(credentialType.get())) {
|
||||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
profileKeyCredentialResponse = getProfileCredential(credentialRequest.get(),
|
||||||
UserCapabilities.createForAccount(accountProfile.get()),
|
profile.get(),
|
||||||
username.orElse(null),
|
requestAccount.get().getUuid());
|
||||||
null,
|
|
||||||
profileBadgeConverter.convert(acceptableLanguages, accountProfile.get().getBadges(), isSelf),
|
pniCredentialResponse = null;
|
||||||
credential.orElse(null)));
|
} else {
|
||||||
} catch (InvalidInputException e) {
|
throw new BadRequestException();
|
||||||
logger.info("Bad profile request", e);
|
}
|
||||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
} else {
|
||||||
|
profileKeyCredentialResponse = null;
|
||||||
|
pniCredentialResponse = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new Profile(
|
||||||
|
name,
|
||||||
|
about,
|
||||||
|
aboutEmoji,
|
||||||
|
avatar,
|
||||||
|
paymentAddress,
|
||||||
|
accountProfile.get().getIdentityKey(),
|
||||||
|
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||||
|
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||||
|
UserCapabilities.createForAccount(accountProfile.get()),
|
||||||
|
username.orElse(null),
|
||||||
|
null,
|
||||||
|
profileBadgeConverter.convert(acceptableLanguages, accountProfile.get().getBadges(), isSelf),
|
||||||
|
profileKeyCredentialResponse,
|
||||||
|
pniCredentialResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -332,28 +358,37 @@ public class ProfileController {
|
||||||
getAcceptableLanguagesForRequest(containerRequestContext),
|
getAcceptableLanguagesForRequest(containerRequestContext),
|
||||||
accountProfile.getBadges(),
|
accountProfile.getBadges(),
|
||||||
isSelf),
|
isSelf),
|
||||||
|
null,
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<ProfileKeyCredentialResponse> getProfileCredential(Optional<String> encodedProfileCredentialRequest,
|
private ProfileKeyCredentialResponse getProfileCredential(final String encodedProfileCredentialRequest,
|
||||||
Optional<VersionedProfile> profile,
|
final VersionedProfile profile,
|
||||||
UUID uuid)
|
final UUID uuid) {
|
||||||
throws InvalidInputException
|
|
||||||
{
|
|
||||||
if (encodedProfileCredentialRequest.isEmpty()) return Optional.empty();
|
|
||||||
if (profile.isEmpty()) return Optional.empty();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.get().getCommitment());
|
final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.getCommitment());
|
||||||
ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedProfileCredentialRequest.get()));
|
final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedProfileCredentialRequest));
|
||||||
ProfileKeyCredentialResponse response = zkProfileOperations.issueProfileKeyCredential(request, uuid, commitment);
|
|
||||||
|
|
||||||
return Optional.of(response);
|
return zkProfileOperations.issueProfileKeyCredential(request, uuid, commitment);
|
||||||
} catch (DecoderException | VerificationFailedException e) {
|
} catch (DecoderException | VerificationFailedException | InvalidInputException e) {
|
||||||
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
|
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PniCredentialResponse getPniCredential(final String encodedCredentialRequest,
|
||||||
|
final VersionedProfile profile,
|
||||||
|
final UUID accountIdentifier,
|
||||||
|
final UUID phoneNumberIdentifier) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.getCommitment());
|
||||||
|
final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedCredentialRequest));
|
||||||
|
|
||||||
|
return zkProfileOperations.issuePniCredential(request, accountIdentifier, phoneNumberIdentifier, commitment);
|
||||||
|
} catch (DecoderException | VerificationFailedException | InvalidInputException e) {
|
||||||
|
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Old profile endpoints. Replaced by versioned profile endpoints (above)
|
// Old profile endpoints. Replaced by versioned profile endpoints (above)
|
||||||
|
|
||||||
|
@ -415,6 +450,7 @@ public class ProfileController {
|
||||||
getAcceptableLanguagesForRequest(containerRequestContext),
|
getAcceptableLanguagesForRequest(containerRequestContext),
|
||||||
accountProfile.get().getBadges(),
|
accountProfile.get().getBadges(),
|
||||||
isSelf),
|
isSelf),
|
||||||
|
null,
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2020 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Base64;
|
||||||
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.zkgroup.profiles.PniCredentialResponse;
|
||||||
|
|
||||||
|
public class PniCredentialResponseAdapter {
|
||||||
|
|
||||||
|
public static class Serializing extends JsonSerializer<PniCredentialResponse> {
|
||||||
|
@Override
|
||||||
|
public void serialize(PniCredentialResponse response, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
|
||||||
|
throws IOException {
|
||||||
|
if (response == null) jsonGenerator.writeNull();
|
||||||
|
else jsonGenerator.writeString(Base64.getEncoder().encodeToString(response.serialize()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Deserializing extends JsonDeserializer<PniCredentialResponse> {
|
||||||
|
@Override
|
||||||
|
public PniCredentialResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
||||||
|
throws IOException {
|
||||||
|
try {
|
||||||
|
return new PniCredentialResponse(Base64.getDecoder().decode(jsonParser.getValueAsString()));
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import org.signal.zkgroup.profiles.PniCredentialResponse;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||||
|
|
||||||
public class Profile {
|
public class Profile {
|
||||||
|
@ -56,12 +57,17 @@ public class Profile {
|
||||||
@JsonDeserialize(using = ProfileKeyCredentialResponseAdapter.Deserializing.class)
|
@JsonDeserialize(using = ProfileKeyCredentialResponseAdapter.Deserializing.class)
|
||||||
private ProfileKeyCredentialResponse credential;
|
private ProfileKeyCredentialResponse credential;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonSerialize(using = PniCredentialResponseAdapter.Serializing.class)
|
||||||
|
@JsonDeserialize(using = PniCredentialResponseAdapter.Deserializing.class)
|
||||||
|
private PniCredentialResponse pniCredential;
|
||||||
|
|
||||||
public Profile() {}
|
public Profile() {}
|
||||||
|
|
||||||
public Profile(
|
public Profile(
|
||||||
String name, String about, String aboutEmoji, String avatar, String paymentAddress, String identityKey,
|
String name, String about, String aboutEmoji, String avatar, String paymentAddress, String identityKey,
|
||||||
String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess, UserCapabilities capabilities, String username,
|
String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess, UserCapabilities capabilities, String username,
|
||||||
UUID uuid, List<Badge> badges, ProfileKeyCredentialResponse credential)
|
UUID uuid, List<Badge> badges, ProfileKeyCredentialResponse credential, PniCredentialResponse pniCredential)
|
||||||
{
|
{
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.about = about;
|
this.about = about;
|
||||||
|
@ -76,6 +82,7 @@ public class Profile {
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.badges = badges;
|
this.badges = badges;
|
||||||
this.credential = credential;
|
this.credential = credential;
|
||||||
|
this.pniCredential = pniCredential;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
Loading…
Reference in New Issue