diff --git a/README.md b/README.md index 9cafdd76..deac801f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,61 @@ signup-service is part of the esignet modules, but has a separate Helm chart so as to install and manage it in a completely independent namespace. +Below diagram depicts the high level deployment architecture for signup service with MOSIP ID-repo. + +![](docs/signup-with-mosip-id-repo.png) + +### Configurations +Signup service and signup UI currently supports default [ID schema](docs/id-schema.json) only. + +**Note:** +Work is in progress to support any ID schema and also to connect with any registry services. + +With respect to the default ID schema, below MOSIP configurations are required to be updated. + +#### admin-default.properties +`` +mosip.admin.masterdata.lang-code=eng,khm +`` + +#### application-default.properties +``` +mosip.mandatory-languages=eng,khm + +mosip.optional-languages= + +mosip.default.template-languages=eng,khm +``` + +#### id-authentication-default.properties +``` +request.idtypes.allowed=UIN,HANDLE + +request.idtypes.allowed.internalauth=UIN + +ida.mosip.external.auth.filter.classes.in.execution.order=io.mosip.authentication.hotlistfilter.impl.PartnerIdHotlistFilterImpl,io.mosip.authentication.hotlistfilter.impl.IndividualIdHotlistFilterImpl,io.mosip.authentication.hotlistfilter.impl.DeviceProviderHotlistFilterImpl,io.mosip.authentication.hotlistfilter.impl.DeviceHotlistFilterImpl,io.mosip.authentication.authtypelockfilter.impl.AuthTypeLockFilterImpl + +mosip.ida.handle-types.regex={ '@phone' : '^\\+91[1-9][0-9]{7,9}@phone$' } +``` + +#### id-repository-default.properties +``` +mosip.idrepo.credential.request.enable-convention-based-id=true + +mosip.idrepo.identity.disable-uin-based-credential-request=true + +mosip.idrepo.vid.disable-support=true + +mosip.identity.fieldid.handle-postfix.mapping={'phone':'@phone'} +``` + +#### kernel-default.properties +`` +mosip.kernel.sms.country.code=+91 +`` + + + ## Installing in k8s cluster using helm ### Pre-requisites 1. Set the kube config file of the Mosip cluster having dependent services is set correctly in PC. @@ -53,6 +108,7 @@ signup-service is part of the esignet modules, but has a separate Helm chart so cd helm ./restart-all.sh ``` + ## APIs API documentation is available [here](https://mosip.stoplight.io/docs/identity-provider/branches/signupV1/t9tvfbteqqokf-e-signet-signup-portal-ap-is). diff --git a/docs/id-schema.json b/docs/id-schema.json new file mode 100644 index 00000000..b4347d09 --- /dev/null +++ b/docs/id-schema.json @@ -0,0 +1,193 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Identity schema for sign up", + "additionalProperties": false, + "title": "signup identity", + "type": "object", + "definitions": { + "simpleType": { + "uniqueItems": true, + "additionalItems": false, + "type": "array", + "items": { + "additionalProperties": false, + "type": "object", + "required": [ + "language", + "value" + ], + "properties": { + "language": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "documentType": { + "additionalProperties": false, + "type": "object", + "properties": { + "format": { + "type": "string" + }, + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "biometricsType": { + "additionalProperties": false, + "type": "object", + "properties": { + "format": { + "type": "string" + }, + "version": { + "type": "number", + "minimum": 0 + }, + "value": { + "type": "string" + } + } + }, + "hashType": { + "additionalProperties": false, + "type": "object", + "properties": { + "hash": { + "type": "string" + }, + "salt": { + "type": "string" + } + } + } + }, + "properties": { + "identity": { + "additionalProperties": false, + "type": "object", + "required": [ + "IDSchemaVersion", + "fullName", + "phone", + "password", + "preferredLang", + "registrationType" + ], + "properties": { + "UIN": { + "bioAttributes": [], + "fieldCategory": "none", + "format": "none", + "type": "string", + "fieldType": "default" + }, + "IDSchemaVersion": { + "bioAttributes": [], + "fieldCategory": "none", + "format": "none", + "type": "number", + "fieldType": "default", + "minimum": 0 + }, + "selectedHandles" : { + "fieldCategory": "none", + "format": "none", + "type": "array", + "items" : { "type" : "string" }, + "fieldType": "default" + }, + "fullName": { + "bioAttributes": [], + "validators": [ + { + "validator": "^(?=.{3,50}$).", + "arguments": [], + "type": "regex" + } + ], + "fieldCategory": "pvt", + "format": "none", + "fieldType": "default", + "$ref": "#/definitions/simpleType" + }, + "phone": { + "bioAttributes": [], + "validators": [ + { + "validator": "^[+]91([0-9]{8,9})$", + "arguments": [], + "type": "regex" + } + ], + "fieldCategory": "pvt", + "format": "none", + "type": "string", + "fieldType": "default", + "requiredOn" : "", + "handle" : true + }, + "password": { + "bioAttributes": [], + "validators": [], + "fieldCategory": "pvt", + "format": "none", + "fieldType": "default", + "$ref": "#/definitions/hashType" + }, + "preferredLang": { + "bioAttributes": [], + "validators": [ + { + "validator": "(^eng$)", + "arguments": [], + "type": "regex" + } + ], + "fieldCategory": "pvt", + "format": "none", + "fieldType": "default", + "type": "string" + }, + "registrationType": { + "bioAttributes": [], + "validators": [ + { + "validator": "^L[1-2]{1}$", + "arguments": [], + "type": "regex" + } + ], + "fieldCategory": "pvt", + "format": "none", + "fieldType": "default", + "type": "string" + }, + "phoneVerified": { + "bioAttributes": [], + "validators": [], + "fieldCategory": "pvt", + "format": "none", + "fieldType": "default", + "type": "boolean" + }, + "updatedAt": { + "bioAttributes": [], + "validators": [], + "fieldCategory": "pvt", + "format": "none", + "fieldType": "default", + "type": "number" + } + } + } + } +} \ No newline at end of file diff --git a/docs/signup-with-mosip-id-repo.png b/docs/signup-with-mosip-id-repo.png new file mode 100644 index 00000000..7698b846 Binary files /dev/null and b/docs/signup-with-mosip-id-repo.png differ diff --git a/signup-service/src/main/java/io/mosip/signup/controllers/RegistrationController.java b/signup-service/src/main/java/io/mosip/signup/controllers/RegistrationController.java index e744188f..3a9475e3 100644 --- a/signup-service/src/main/java/io/mosip/signup/controllers/RegistrationController.java +++ b/signup-service/src/main/java/io/mosip/signup/controllers/RegistrationController.java @@ -1,5 +1,7 @@ package io.mosip.signup.controllers; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.annotation.Timed; import io.mosip.esignet.core.dto.RequestWrapper; import io.mosip.esignet.core.dto.ResponseWrapper; import io.mosip.esignet.core.util.IdentityProviderUtil; @@ -70,6 +72,8 @@ public ResponseWrapper verifyChallenge(@Valid @RequestB return responseWrapper; } + + @Timed(value = "register.timer", percentiles = {0.95, 0.99}) @PostMapping("/register") public ResponseWrapper register(@Valid @RequestBody RequestWrapper requestWrapper, @Valid @NotBlank(message = ErrorConstants.INVALID_TRANSACTION) diff --git a/signup-service/src/main/java/io/mosip/signup/controllers/SignUpController.java b/signup-service/src/main/java/io/mosip/signup/controllers/SignUpController.java index 8af399ea..ee177671 100644 --- a/signup-service/src/main/java/io/mosip/signup/controllers/SignUpController.java +++ b/signup-service/src/main/java/io/mosip/signup/controllers/SignUpController.java @@ -1,6 +1,7 @@ package io.mosip.signup.controllers; +import io.micrometer.core.annotation.Timed; import io.mosip.esignet.core.dto.RequestWrapper; import io.mosip.esignet.core.dto.ResponseWrapper; import io.mosip.esignet.core.util.IdentityProviderUtil; @@ -43,6 +44,7 @@ public ResponseWrapper getSignUpDetails() { return responseWrapper; } + @Timed(value = "resetpwd.timer", percentiles = {0.95, 0.99}) @PostMapping("/reset-password") public ResponseWrapper resetPassword(@Valid @RequestBody RequestWrapper requestWrapper, @Valid @NotBlank(message = ErrorConstants.INVALID_TRANSACTION) diff --git a/signup-service/src/main/java/io/mosip/signup/services/CacheUtilService.java b/signup-service/src/main/java/io/mosip/signup/services/CacheUtilService.java index 75a0b9da..8cb2810e 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/CacheUtilService.java +++ b/signup-service/src/main/java/io/mosip/signup/services/CacheUtilService.java @@ -22,11 +22,6 @@ public class CacheUtilService { CacheManager cacheManager; //---Setter--- - @Cacheable(value = SignUpConstants.CHALLENGE_GENERATED, key = "#transactionId") - public RegistrationTransaction setChallengeGeneratedTransaction(String transactionId, - RegistrationTransaction registrationTransaction) { - return registrationTransaction; - } @CacheEvict(value = SignUpConstants.CHALLENGE_GENERATED, key = "#transactionId") @Cacheable(value = SignUpConstants.CHALLENGE_VERIFIED, key = "#verifiedTransactionId") @@ -36,14 +31,15 @@ public RegistrationTransaction setChallengeVerifiedTransaction(String transactio } @CacheEvict(value = SignUpConstants.CHALLENGE_VERIFIED, key = "#transactionId") - @CachePut(value = SignUpConstants.STATUS_CHECK, key = "#transactionId") + @Cacheable(value = SignUpConstants.STATUS_CHECK, key = "#transactionId") public RegistrationTransaction setStatusCheckTransaction(String transactionId, RegistrationTransaction registrationTransaction) { return registrationTransaction; } + @CacheEvict(value = SignUpConstants.CHALLENGE_GENERATED, key = "#transactionId") @Cacheable(value = SignUpConstants.BLOCKED_IDENTIFIER, key = "#key") - public String blockIdentifier(String key, String value) { + public String blockIdentifier(String transactionId, String key, String value) { return value; } @@ -52,11 +48,26 @@ public String setSecretKey(String key, String secretKey) { return secretKey; } - @Cacheable(value = SignUpConstants.KEY_ALIAS, key = "#key") + @CachePut(value = SignUpConstants.KEY_ALIAS, key = "#key") public String setActiveKeyAlias(String key, String alias) { return alias; } + + //----- cache update is separated + //----- we are not using @cacheput as @cacheput extends the TTL on cache entry + + public RegistrationTransaction createUpdateChallengeGeneratedTransaction(String transactionId, + RegistrationTransaction registrationTransaction) { + cacheManager.getCache(SignUpConstants.CHALLENGE_GENERATED).put(transactionId, registrationTransaction); + return registrationTransaction; + } + + public void updateStatusCheckTransaction(String transactionId, + RegistrationTransaction registrationTransaction) { + cacheManager.getCache(SignUpConstants.STATUS_CHECK).put(transactionId, registrationTransaction); + } + //---Getter--- public RegistrationTransaction getChallengeGeneratedTransaction(String transactionId) { return cacheManager.getCache(SignUpConstants.CHALLENGE_GENERATED).get(transactionId, RegistrationTransaction.class); diff --git a/signup-service/src/main/java/io/mosip/signup/services/ChallengeManagerService.java b/signup-service/src/main/java/io/mosip/signup/services/ChallengeManagerService.java index a2c8bd85..0d98e698 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/ChallengeManagerService.java +++ b/signup-service/src/main/java/io/mosip/signup/services/ChallengeManagerService.java @@ -1,5 +1,6 @@ package io.mosip.signup.services; +import io.micrometer.core.annotation.Timed; import io.mosip.esignet.core.util.IdentityProviderUtil; import io.mosip.signup.dto.*; import io.mosip.signup.exception.SignUpException; @@ -39,6 +40,7 @@ public String generateChallenge(RegistrationTransaction transaction) throws Sign throw new SignUpException(ErrorConstants.UNSUPPORTED_CHALLENGE_TYPE); } + @Timed(value = "generateotp.api.timer", percentiles = {0.95, 0.99}) private String generateOTPChallenge(String challengeTransactionId) { OtpRequest otpRequest = new OtpRequest(); otpRequest.setKey(challengeTransactionId); diff --git a/signup-service/src/main/java/io/mosip/signup/services/GoogleRecaptchaValidatorService.java b/signup-service/src/main/java/io/mosip/signup/services/GoogleRecaptchaValidatorService.java index 281c4f71..fdbb9474 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/GoogleRecaptchaValidatorService.java +++ b/signup-service/src/main/java/io/mosip/signup/services/GoogleRecaptchaValidatorService.java @@ -1,5 +1,6 @@ package io.mosip.signup.services; +import io.micrometer.core.annotation.Timed; import io.mosip.esignet.api.spi.CaptchaValidator; import io.mosip.signup.dto.ReCaptchaResponse; import lombok.extern.slf4j.Slf4j; @@ -30,6 +31,7 @@ public class GoogleRecaptchaValidatorService implements CaptchaValidator { @Autowired private RestTemplate restTemplate; + @Timed(value = "validatecaptcha.api.timer", percentiles = {0.95, 0.99}) @Override public boolean validateCaptcha(String captchaToken) { diff --git a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java index d888751e..c7979b66 100644 --- a/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java +++ b/signup-service/src/main/java/io/mosip/signup/services/RegistrationService.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.micrometer.core.annotation.Timed; import io.mosip.esignet.core.util.IdentityProviderUtil; import io.mosip.kernel.core.util.HMACUtils2; import io.mosip.signup.dto.*; @@ -146,7 +147,7 @@ public GenerateChallengeResponse generateChallenge(GenerateChallengeRequest gene } else { transaction = cacheUtilService.getChallengeGeneratedTransaction(transactionId); - validateTransaction(transaction, identifier); + validateTransaction(transaction, identifier, generateChallengeRequest); } // generate Challenge @@ -155,11 +156,11 @@ public GenerateChallengeResponse generateChallenge(GenerateChallengeRequest gene transaction.setChallengeHash(challengeHash); transaction.increaseAttempt(); transaction.setLocale(generateChallengeRequest.getLocale()); - cacheUtilService.setChallengeGeneratedTransaction(transactionId, transaction); + cacheUtilService.createUpdateChallengeGeneratedTransaction(transactionId, transaction); //Resend attempts exhausted, block the identifier for configured time. if(transaction.getChallengeRetryAttempts() > resendAttempts) - cacheUtilService.blockIdentifier(transaction.getIdentifier(), "blocked"); + cacheUtilService.blockIdentifier(transactionId, transaction.getIdentifier(), "blocked"); notificationHelper.sendSMSNotificationAsync(generateChallengeRequest.getIdentifier(), transaction.getLocale(), SEND_OTP_SMS_NOTIFICATION_TEMPLATE_KEY, new HashMap<>(){{put("{challenge}", challenge);}}) @@ -265,10 +266,15 @@ public RegistrationStatusResponse updatePassword(ResetPasswordRequest resetPassw } if(!transaction.isValidIdentifier(resetPasswordRequest.getIdentifier().toLowerCase(Locale.ROOT))) { - log.error("generate-challenge failed: invalid identifier"); + log.error("reset password failed: invalid identifier"); throw new SignUpException(ErrorConstants.IDENTIFIER_MISMATCH); } + if(!transaction.getPurpose().equals(Purpose.RESET_PASSWORD)) { + log.error("reset password failed: purpose mismatch in transaction"); + throw new SignUpException(ErrorConstants.UNSUPPORTED_PURPOSE); + } + Identity identity = new Identity(); identity.setUIN(cryptoHelper.symmetricDecrypt(transaction.getUin())); identity.setIDSchemaVersion(idSchemaVersion); @@ -339,7 +345,7 @@ public RegistrationStatusResponse getRegistrationStatus(String transactionId) registrationTransaction.getHandlesStatus().put(handleRequestId, registrationStatus); //TODO This is temporary fix, we need to remove this field later from registrationTransaction DTO. registrationTransaction.setRegistrationStatus(registrationStatus); - cacheUtilService.setStatusCheckTransaction(transactionId, registrationTransaction); + cacheUtilService.updateStatusCheckTransaction(transactionId, registrationTransaction); } } registrationTransaction = cacheUtilService.getStatusCheckTransaction(transactionId); @@ -450,6 +456,7 @@ private void saveIdentityData(RegisterRequest registerRequest, String transactio addIdentity(identityRequest, transactionId); } + @Timed(value = "addidentity.api.timer", percentiles = {0.95, 0.99}) private void addIdentity(IdentityRequest identityRequest, String transactionId) throws SignUpException{ RestRequestWrapper restRequest = new RestRequestWrapper<>(); @@ -475,6 +482,7 @@ private void addIdentity(IdentityRequest identityRequest, String transactionId) restResponseWrapper.getErrors().get(0).getErrorCode() : ErrorConstants.ADD_IDENTITY_FAILED); } + @Timed(value = "generatehash.api.timer", percentiles = {0.95, 0.99}) private Password generateSaltedHash(String password, String transactionId) throws SignUpException{ RestRequestWrapper restRequestWrapper = new RestRequestWrapper<>(); @@ -497,6 +505,7 @@ private Password generateSaltedHash(String password, String transactionId) throw restResponseWrapper.getErrors().get(0).getErrorCode() : ErrorConstants.HASH_GENERATE_FAILED); } + @Timed(value = "getuin.api.timer", percentiles = {0.95, 0.99}) private String getUniqueIdentifier(String transactionId) throws SignUpException { RestResponseWrapper restResponseWrapper = selfTokenRestTemplate.exchange(getUinEndpoint, @@ -513,7 +522,8 @@ private String getUniqueIdentifier(String transactionId) throws SignUpException restResponseWrapper.getErrors().get(0).getErrorCode() : ErrorConstants.GET_UIN_FAILED); } - private void validateTransaction(RegistrationTransaction transaction, String identifier) { + private void validateTransaction(RegistrationTransaction transaction, String identifier, + GenerateChallengeRequest generateChallengeRequest) { if(transaction == null) { log.error("generate-challenge failed: validate transaction null"); throw new InvalidTransactionException(); @@ -533,8 +543,14 @@ private void validateTransaction(RegistrationTransaction transaction, String ide log.error("generate-challenge failed: too early attempts"); throw new GenerateChallengeException(ErrorConstants.TOO_EARLY_ATTEMPT); } + + if(!transaction.getPurpose().equals(generateChallengeRequest.getPurpose())) { + log.error("generate-challenge failed: purpose mismatch"); + throw new GenerateChallengeException(ErrorConstants.INVALID_PURPOSE); + } } + @Timed(value = "getstatus.api.timer", percentiles = {0.95, 0.99}) private RegistrationStatus getRegistrationStatusFromServer(String applicationId) { RestResponseWrapper> restResponseWrapper = selfTokenRestTemplate.exchange(getRegistrationStatusEndpoint, HttpMethod.GET, null, diff --git a/signup-service/src/test/java/io/mosip/signup/services/CacheUtilServiceTest.java b/signup-service/src/test/java/io/mosip/signup/services/CacheUtilServiceTest.java index ca32be57..3fc66470 100644 --- a/signup-service/src/test/java/io/mosip/signup/services/CacheUtilServiceTest.java +++ b/signup-service/src/test/java/io/mosip/signup/services/CacheUtilServiceTest.java @@ -31,7 +31,7 @@ public void test_RegistrationTransaction_cache() { Mockito.when(cache.get("mock", RegistrationTransaction.class)).thenReturn(registrationTransaction); Mockito.when(cacheManager.getCache(Mockito.anyString())).thenReturn(cache); - Assert.assertEquals(cacheUtilService.setChallengeGeneratedTransaction("mock", + Assert.assertEquals(cacheUtilService.createUpdateChallengeGeneratedTransaction("mock", registrationTransaction), registrationTransaction); Assert.assertEquals(cacheUtilService.setChallengeVerifiedTransaction("mock", "vmock", registrationTransaction), registrationTransaction); @@ -45,8 +45,9 @@ public void test_RegistrationTransaction_cache() { @Test public void setChallengeTransaction_thenPass() { + Mockito.when(cacheManager.getCache(Mockito.anyString())).thenReturn(cache); RegistrationTransaction registrationTransaction = new RegistrationTransaction("+85512123123", Purpose.REGISTRATION); - Assert.assertEquals(cacheUtilService.setChallengeGeneratedTransaction("mock-transaction", registrationTransaction), registrationTransaction); - Assert.assertNotNull(cacheUtilService.setChallengeGeneratedTransaction("mock-transaction", registrationTransaction)); + Assert.assertEquals(cacheUtilService.createUpdateChallengeGeneratedTransaction("mock-transaction", registrationTransaction), registrationTransaction); + Assert.assertNotNull(cacheUtilService.createUpdateChallengeGeneratedTransaction("mock-transaction", registrationTransaction)); } } diff --git a/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java b/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java index 0fca5cfb..c937e56f 100644 --- a/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java +++ b/signup-service/src/test/java/io/mosip/signup/services/RegistrationServiceTest.java @@ -1639,6 +1639,7 @@ public void doGenerateChallenge_withRetryAttemptsOver3time_thenFail() throws Sig generateChallengeRequest.setIdentifier(identifier); generateChallengeRequest.setCaptchaToken("mock-captcha"); generateChallengeRequest.setRegenerate(true); + generateChallengeRequest.setPurpose(Purpose.REGISTRATION); String transactionId = "TRAN-1234"; RegistrationTransaction transaction = new RegistrationTransaction(identifier, Purpose.REGISTRATION); transaction.setLastRetryAt(LocalDateTime.now(ZoneOffset.UTC).minusSeconds(40)); @@ -1665,6 +1666,7 @@ public void doGenerateChallenge_withTransactionId_thenPass() throws SignUpExcept generateChallengeRequest.setIdentifier(identifier); generateChallengeRequest.setCaptchaToken("mock-captcha"); generateChallengeRequest.setRegenerate(true); + generateChallengeRequest.setPurpose(Purpose.REGISTRATION); String transactionId = "TRAN-1234"; RegistrationTransaction transaction = new RegistrationTransaction(identifier, Purpose.REGISTRATION); transaction.setLastRetryAt(LocalDateTime.now(ZoneOffset.UTC).minusSeconds(40)); diff --git a/signup-ui/public/locales/en.json b/signup-ui/public/locales/en.json index ef8bc50f..1af962ab 100644 --- a/signup-ui/public/locales/en.json +++ b/signup-ui/public/locales/en.json @@ -118,7 +118,7 @@ "already-registered": "Already Registered", "timed_out": "Timedout", "timeout_transaction": "Transaction Timeout", - "request_limit": "Request Limit", + "request_limit": "It is taking time to fetch your account status. Please try login after 5 minutes.", "not_registered": "Not Registered", "reset_pwd_failed": "Reset Password Failed", "identifier_already_registered": "Identifier already registered", diff --git a/signup-ui/public/locales/km.json b/signup-ui/public/locales/km.json index b1bc3b3a..b7637dae 100644 --- a/signup-ui/public/locales/km.json +++ b/signup-ui/public/locales/km.json @@ -118,7 +118,7 @@ "already-registered": "បានចុះឈ្មោះរួចហើយ", "timed_out": "អស់ម៉ោង", "timeout_transaction": "ប្រតិបត្តិការផុតកំណត់", - "request_limit": "ចំនួនសំណើដល់កំណត់", + "request_limit": "ត្រូវការពេលវេលាដើម្បីទាញយកស្ថានភាពគណនីរបស់អ្នក។ សូមព្យាយាមចូលប្រើបន្ទាប់ពី 5 នាទី។", "not_registered": "មិនបានចុះឈ្មោះ", "reset_pwd_failed": "ការកំណត់ពាក្យសម្ងាត់ឡើងវិញបានបរាជ័យ", "identifier_already_registered": "អត្តសញ្ញាណបានចុះឈ្មោះរួចហើយ",