diff --git a/cmd/eskimo-hut/api/docs.go b/cmd/eskimo-hut/api/docs.go index c281e58f..df8e8b8e 100644 --- a/cmd/eskimo-hut/api/docs.go +++ b/cmd/eskimo-hut/api/docs.go @@ -20,7 +20,625 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/auth/getMetadata": { + "/v1r/user-statistics/top-countries": { + "get": { + "description": "Returns the paginated view of users per country.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statistics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "a keyword to look for in all country codes or names", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "Limit of elements to return. Defaults to 10", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of elements to skip before collecting elements to return", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/users.CountryStatistics" + } + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/user-statistics/user-growth": { + "get": { + "description": "Returns statistics about user growth.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statistics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "integer", + "description": "number of days in the past to look for. Defaults to 3. Max is 90.", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Timezone in format +04:30 or -03:45", + "name": "tz", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/users.UserGrowthStatistics" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/user-views/username": { + "get": { + "description": "Returns public information about an user account based on an username, making sure the username is valid first.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "username of the user. It will validate it first", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.UserProfile" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/users": { + "get": { + "description": "Returns a list of user account based on the provided query parameters.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "A keyword to look for in the usernames", + "name": "keyword", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Limit of elements to return. Defaults to 10", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Elements to skip before starting to look for", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/users.MinimalUserProfile" + } + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/users/{userId}": { + "get": { + "description": "Returns an user's account.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.UserProfile" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/users/{userId}/referral-acquisition-history": { + "get": { + "description": "Returns the history of referral acquisition for the provided user id.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Referrals" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Always is 5, cannot be changed due to DB schema", + "name": "days", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/users.ReferralAcquisition" + } + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/users/{userId}/referrals": { + "get": { + "description": "Returns the referrals of an user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Referrals" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Type of referrals: ` + "`" + `CONTACTS` + "`" + ` or ` + "`" + `T1` + "`" + ` or ` + "`" + `T2` + "`" + `", + "name": "type", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Limit of elements to return. Defaults to 10", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of elements to skip before collecting elements to return", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/users.Referrals" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1w/auth/getMetadata": { "post": { "description": "Fetches user's metadata based on token's data", "produces": [ @@ -67,7 +685,7 @@ const docTemplate = `{ } } }, - "/auth/getValidUserForPhoneNumberMigration": { + "/v1w/auth/getValidUserForPhoneNumberMigration": { "post": { "description": "Returns minimal user information based on provided phone number, in the context of migrating a phone number only account to an email one.", "consumes": [ @@ -146,7 +764,7 @@ const docTemplate = `{ } } }, - "/auth/processFaceRecognitionResult": { + "/v1w/auth/processFaceRecognitionResult": { "post": { "description": "Webhook to notify the service about the result of an user's face authentication process.", "consumes": [ @@ -235,7 +853,7 @@ const docTemplate = `{ } } }, - "/auth/refreshTokens": { + "/v1w/auth/refreshTokens": { "post": { "description": "Issues new access token", "consumes": [ @@ -312,7 +930,7 @@ const docTemplate = `{ } } }, - "/auth/sendSignInLinkToEmail": { + "/v1w/auth/sendSignInLinkToEmail": { "post": { "description": "Starts email link auth process", "consumes": [ @@ -396,7 +1014,7 @@ const docTemplate = `{ } } }, - "/auth/signInWithConfirmationCode": { + "/v1w/auth/signInWithConfirmationCode": { "post": { "description": "Finishes login flow using confirmation code", "produces": [ @@ -456,7 +1074,7 @@ const docTemplate = `{ } } }, - "/kyc/checkKYCStep4Status/users/{userId}": { + "/v1w/kyc/checkKYCStep4Status/users/{userId}": { "post": { "description": "Checks the status of the quiz kyc step (4).", "consumes": [ @@ -544,7 +1162,7 @@ const docTemplate = `{ } } }, - "/kyc/startOrContinueKYCStep4Session/users/{userId}": { + "/v1w/kyc/startOrContinueKYCStep4Session/users/{userId}": { "post": { "description": "Starts or continues the kyc 4 session (Quiz), if available and if not already finished successfully.", "consumes": [ @@ -659,7 +1277,7 @@ const docTemplate = `{ } } }, - "/kyc/tryResetKYCSteps/users/{userId}": { + "/v1w/kyc/tryResetKYCSteps/users/{userId}": { "post": { "description": "Checks if there are any kyc steps that should be reset, if so, it resets them and returns the updated latest user state.", "consumes": [ @@ -757,7 +1375,7 @@ const docTemplate = `{ } } }, - "/kyc/verifySocialKYCStep/users/{userId}": { + "/v1w/kyc/verifySocialKYCStep/users/{userId}": { "post": { "description": "Verifies if the user has posted the expected verification post on their social media account.", "consumes": [ @@ -894,7 +1512,7 @@ const docTemplate = `{ } } }, - "/users": { + "/v1w/users": { "post": { "description": "Creates an user account", "consumes": [ @@ -991,7 +1609,7 @@ const docTemplate = `{ } } }, - "/users/{userId}": { + "/v1w/users/{userId}": { "delete": { "description": "Deletes an user account", "consumes": [ @@ -1293,7 +1911,7 @@ const docTemplate = `{ } } }, - "/users/{userId}/devices/{deviceUniqueId}/metadata": { + "/v1w/users/{userId}/devices/{deviceUniqueId}/metadata": { "put": { "description": "Replaces existing device metadata with the provided one.", "consumes": [ @@ -1401,7 +2019,7 @@ const docTemplate = `{ } } }, - "/users/{userId}/devices/{deviceUniqueId}/metadata/location": { + "/v1w/users/{userId}/devices/{deviceUniqueId}/metadata/location": { "put": { "description": "Returns the device's geolocation based on its IP or based on account information if userId is also provided.", "consumes": [ @@ -2072,6 +2690,150 @@ const docTemplate = `{ } } }, + "main.UserProfile": { + "type": "object", + "properties": { + "agendaPhoneNumberHashes": { + "type": "string", + "example": "Ef86A6021afCDe5673511376B2,Ef86A6021afCDe5673511376B2,Ef86A6021afCDe5673511376B2,Ef86A6021afCDe5673511376B2" + }, + "blockchainAccountAddress": { + "type": "string", + "example": "0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "checksum": { + "type": "string", + "example": "1232412415326543647657" + }, + "city": { + "type": "string", + "example": "New York" + }, + "clientData": { + "$ref": "#/definitions/users.JSON" + }, + "country": { + "type": "string", + "example": "US" + }, + "createdAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "email": { + "type": "string", + "example": "jdoe@gmail.com" + }, + "firstName": { + "type": "string", + "example": "John" + }, + "hiddenProfileElements": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "globalRank", + "referralCount", + "level", + "role", + "badges" + ] + }, + "example": [ + "level" + ] + }, + "id": { + "type": "string", + "example": "did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "kycStepBlocked": { + "allOf": [ + { + "$ref": "#/definitions/users.KYCStep" + } + ], + "example": 0 + }, + "kycStepPassed": { + "allOf": [ + { + "$ref": "#/definitions/users.KYCStep" + } + ], + "example": 0 + }, + "kycStepsCreatedAt": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "2022-01-03T16:20:52.156534Z" + ] + }, + "kycStepsLastUpdatedAt": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "2022-01-03T16:20:52.156534Z" + ] + }, + "language": { + "type": "string", + "example": "en" + }, + "lastName": { + "type": "string", + "example": "Doe" + }, + "miningBlockchainAccountAddress": { + "type": "string", + "example": "0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "phoneNumber": { + "type": "string", + "example": "+12099216581" + }, + "profilePictureUrl": { + "type": "string", + "example": "https://somecdn.com/p1.jpg" + }, + "referredBy": { + "type": "string", + "example": "did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "repeatableKYCSteps": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "t1ReferralCount": { + "type": "integer", + "example": 100 + }, + "t2ReferralCount": { + "type": "integer", + "example": 100 + }, + "updatedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "username": { + "type": "string", + "example": "jdoe" + }, + "verified": { + "type": "boolean", + "example": true + } + } + }, "quiz.Progress": { "type": "object", "properties": { @@ -2250,6 +3012,20 @@ const docTemplate = `{ "FailureVerificationResult" ] }, + "users.CountryStatistics": { + "type": "object", + "properties": { + "country": { + "description": "ISO 3166 country code.", + "type": "string", + "example": "US" + }, + "userCount": { + "type": "integer", + "example": 12121212 + } + } + }, "users.DeviceLocation": { "type": "object", "properties": { @@ -2295,6 +3071,152 @@ const docTemplate = `{ "Social6KYCStep", "Social7KYCStep" ] + }, + "users.MinimalUserProfile": { + "type": "object", + "properties": { + "active": { + "type": "boolean", + "example": true + }, + "city": { + "type": "string", + "example": "New York" + }, + "country": { + "type": "string", + "example": "US" + }, + "email": { + "type": "string", + "example": "jdoe@gmail.com" + }, + "id": { + "type": "string", + "example": "did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "phoneNumber": { + "type": "string", + "example": "+12099216581" + }, + "pinged": { + "type": "boolean", + "example": false + }, + "profilePictureUrl": { + "type": "string", + "example": "https://somecdn.com/p1.jpg" + }, + "referralType": { + "enum": [ + "CONTACTS", + "T0", + "T1", + "T2" + ], + "allOf": [ + { + "$ref": "#/definitions/users.ReferralType" + } + ], + "example": "T1" + }, + "username": { + "type": "string", + "example": "jdoe" + }, + "verified": { + "type": "boolean", + "example": true + } + } + }, + "users.ReferralAcquisition": { + "type": "object", + "properties": { + "date": { + "type": "string", + "example": "2022-01-03" + }, + "t1": { + "type": "integer", + "example": 22 + }, + "t2": { + "type": "integer", + "example": 13 + } + } + }, + "users.ReferralType": { + "type": "string", + "enum": [ + "CONTACTS", + "T1", + "T2", + "TEAM" + ], + "x-enum-varnames": [ + "ContactsReferrals", + "Tier1Referrals", + "Tier2Referrals", + "TeamReferrals" + ] + }, + "users.Referrals": { + "type": "object", + "properties": { + "active": { + "type": "integer", + "example": 11 + }, + "referrals": { + "type": "array", + "items": { + "$ref": "#/definitions/users.MinimalUserProfile" + } + }, + "total": { + "type": "integer", + "example": 11 + } + } + }, + "users.UserCountTimeSeriesDataPoint": { + "type": "object", + "properties": { + "active": { + "type": "integer", + "example": 11 + }, + "date": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "total": { + "type": "integer", + "example": 11 + } + } + }, + "users.UserGrowthStatistics": { + "type": "object", + "properties": { + "active": { + "type": "integer", + "example": 11 + }, + "timeSeries": { + "type": "array", + "items": { + "$ref": "#/definitions/users.UserCountTimeSeriesDataPoint" + } + }, + "total": { + "type": "integer", + "example": 11 + } + } } } }` @@ -2303,10 +3225,10 @@ const docTemplate = `{ var SwaggerInfo = &swag.Spec{ Version: "latest", Host: "", - BasePath: "/v1w", + BasePath: "", Schemes: []string{"https"}, Title: "User Accounts, User Devices, User Statistics API", - Description: "API that handles everything related to write only operations for user's account, user's devices and statistics about those.", + Description: "API that handles everything related to user's account, user's devices and statistics about those.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/cmd/eskimo-hut/api/swagger.json b/cmd/eskimo-hut/api/swagger.json index fbf4fde7..89f1aca1 100644 --- a/cmd/eskimo-hut/api/swagger.json +++ b/cmd/eskimo-hut/api/swagger.json @@ -4,7 +4,7 @@ ], "swagger": "2.0", "info": { - "description": "API that handles everything related to write only operations for user's account, user's devices and statistics about those.", + "description": "API that handles everything related to user's account, user's devices and statistics about those.", "title": "User Accounts, User Devices, User Statistics API", "contact": { "name": "ice.io", @@ -12,9 +12,626 @@ }, "version": "latest" }, - "basePath": "/v1w", "paths": { - "/auth/getMetadata": { + "/v1r/user-statistics/top-countries": { + "get": { + "description": "Returns the paginated view of users per country.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statistics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "a keyword to look for in all country codes or names", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "Limit of elements to return. Defaults to 10", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of elements to skip before collecting elements to return", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/users.CountryStatistics" + } + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/user-statistics/user-growth": { + "get": { + "description": "Returns statistics about user growth.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statistics" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "integer", + "description": "number of days in the past to look for. Defaults to 3. Max is 90.", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Timezone in format +04:30 or -03:45", + "name": "tz", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/users.UserGrowthStatistics" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/user-views/username": { + "get": { + "description": "Returns public information about an user account based on an username, making sure the username is valid first.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "username of the user. It will validate it first", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.UserProfile" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/users": { + "get": { + "description": "Returns a list of user account based on the provided query parameters.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "A keyword to look for in the usernames", + "name": "keyword", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Limit of elements to return. Defaults to 10", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Elements to skip before starting to look for", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/users.MinimalUserProfile" + } + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/users/{userId}": { + "get": { + "description": "Returns an user's account.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Accounts" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.UserProfile" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "404": { + "description": "if not found", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/users/{userId}/referral-acquisition-history": { + "get": { + "description": "Returns the history of referral acquisition for the provided user id.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Referrals" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Always is 5, cannot be changed due to DB schema", + "name": "days", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/users.ReferralAcquisition" + } + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1r/users/{userId}/referrals": { + "get": { + "description": "Returns the referrals of an user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Referrals" + ], + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "\u003cAdd metadata token here\u003e", + "description": "Insert your metadata token", + "name": "X-Account-Metadata", + "in": "header" + }, + { + "type": "string", + "description": "ID of the user", + "name": "userId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Type of referrals: `CONTACTS` or `T1` or `T2`", + "name": "type", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Limit of elements to return. Defaults to 10", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of elements to skip before collecting elements to return", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/users.Referrals" + } + }, + "400": { + "description": "if validations fail", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "401": { + "description": "if not authorized", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "403": { + "description": "if not allowed", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "422": { + "description": "if syntax fails", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, + "/v1w/auth/getMetadata": { "post": { "description": "Fetches user's metadata based on token's data", "produces": [ @@ -61,7 +678,7 @@ } } }, - "/auth/getValidUserForPhoneNumberMigration": { + "/v1w/auth/getValidUserForPhoneNumberMigration": { "post": { "description": "Returns minimal user information based on provided phone number, in the context of migrating a phone number only account to an email one.", "consumes": [ @@ -140,7 +757,7 @@ } } }, - "/auth/processFaceRecognitionResult": { + "/v1w/auth/processFaceRecognitionResult": { "post": { "description": "Webhook to notify the service about the result of an user's face authentication process.", "consumes": [ @@ -229,7 +846,7 @@ } } }, - "/auth/refreshTokens": { + "/v1w/auth/refreshTokens": { "post": { "description": "Issues new access token", "consumes": [ @@ -306,7 +923,7 @@ } } }, - "/auth/sendSignInLinkToEmail": { + "/v1w/auth/sendSignInLinkToEmail": { "post": { "description": "Starts email link auth process", "consumes": [ @@ -390,7 +1007,7 @@ } } }, - "/auth/signInWithConfirmationCode": { + "/v1w/auth/signInWithConfirmationCode": { "post": { "description": "Finishes login flow using confirmation code", "produces": [ @@ -450,7 +1067,7 @@ } } }, - "/kyc/checkKYCStep4Status/users/{userId}": { + "/v1w/kyc/checkKYCStep4Status/users/{userId}": { "post": { "description": "Checks the status of the quiz kyc step (4).", "consumes": [ @@ -538,7 +1155,7 @@ } } }, - "/kyc/startOrContinueKYCStep4Session/users/{userId}": { + "/v1w/kyc/startOrContinueKYCStep4Session/users/{userId}": { "post": { "description": "Starts or continues the kyc 4 session (Quiz), if available and if not already finished successfully.", "consumes": [ @@ -653,7 +1270,7 @@ } } }, - "/kyc/tryResetKYCSteps/users/{userId}": { + "/v1w/kyc/tryResetKYCSteps/users/{userId}": { "post": { "description": "Checks if there are any kyc steps that should be reset, if so, it resets them and returns the updated latest user state.", "consumes": [ @@ -751,7 +1368,7 @@ } } }, - "/kyc/verifySocialKYCStep/users/{userId}": { + "/v1w/kyc/verifySocialKYCStep/users/{userId}": { "post": { "description": "Verifies if the user has posted the expected verification post on their social media account.", "consumes": [ @@ -888,7 +1505,7 @@ } } }, - "/users": { + "/v1w/users": { "post": { "description": "Creates an user account", "consumes": [ @@ -985,7 +1602,7 @@ } } }, - "/users/{userId}": { + "/v1w/users/{userId}": { "delete": { "description": "Deletes an user account", "consumes": [ @@ -1287,7 +1904,7 @@ } } }, - "/users/{userId}/devices/{deviceUniqueId}/metadata": { + "/v1w/users/{userId}/devices/{deviceUniqueId}/metadata": { "put": { "description": "Replaces existing device metadata with the provided one.", "consumes": [ @@ -1395,7 +2012,7 @@ } } }, - "/users/{userId}/devices/{deviceUniqueId}/metadata/location": { + "/v1w/users/{userId}/devices/{deviceUniqueId}/metadata/location": { "put": { "description": "Returns the device's geolocation based on its IP or based on account information if userId is also provided.", "consumes": [ @@ -2066,6 +2683,150 @@ } } }, + "main.UserProfile": { + "type": "object", + "properties": { + "agendaPhoneNumberHashes": { + "type": "string", + "example": "Ef86A6021afCDe5673511376B2,Ef86A6021afCDe5673511376B2,Ef86A6021afCDe5673511376B2,Ef86A6021afCDe5673511376B2" + }, + "blockchainAccountAddress": { + "type": "string", + "example": "0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "checksum": { + "type": "string", + "example": "1232412415326543647657" + }, + "city": { + "type": "string", + "example": "New York" + }, + "clientData": { + "$ref": "#/definitions/users.JSON" + }, + "country": { + "type": "string", + "example": "US" + }, + "createdAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "email": { + "type": "string", + "example": "jdoe@gmail.com" + }, + "firstName": { + "type": "string", + "example": "John" + }, + "hiddenProfileElements": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "globalRank", + "referralCount", + "level", + "role", + "badges" + ] + }, + "example": [ + "level" + ] + }, + "id": { + "type": "string", + "example": "did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "kycStepBlocked": { + "allOf": [ + { + "$ref": "#/definitions/users.KYCStep" + } + ], + "example": 0 + }, + "kycStepPassed": { + "allOf": [ + { + "$ref": "#/definitions/users.KYCStep" + } + ], + "example": 0 + }, + "kycStepsCreatedAt": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "2022-01-03T16:20:52.156534Z" + ] + }, + "kycStepsLastUpdatedAt": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "2022-01-03T16:20:52.156534Z" + ] + }, + "language": { + "type": "string", + "example": "en" + }, + "lastName": { + "type": "string", + "example": "Doe" + }, + "miningBlockchainAccountAddress": { + "type": "string", + "example": "0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "phoneNumber": { + "type": "string", + "example": "+12099216581" + }, + "profilePictureUrl": { + "type": "string", + "example": "https://somecdn.com/p1.jpg" + }, + "referredBy": { + "type": "string", + "example": "did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "repeatableKYCSteps": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "t1ReferralCount": { + "type": "integer", + "example": 100 + }, + "t2ReferralCount": { + "type": "integer", + "example": 100 + }, + "updatedAt": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "username": { + "type": "string", + "example": "jdoe" + }, + "verified": { + "type": "boolean", + "example": true + } + } + }, "quiz.Progress": { "type": "object", "properties": { @@ -2244,6 +3005,20 @@ "FailureVerificationResult" ] }, + "users.CountryStatistics": { + "type": "object", + "properties": { + "country": { + "description": "ISO 3166 country code.", + "type": "string", + "example": "US" + }, + "userCount": { + "type": "integer", + "example": 12121212 + } + } + }, "users.DeviceLocation": { "type": "object", "properties": { @@ -2289,6 +3064,152 @@ "Social6KYCStep", "Social7KYCStep" ] + }, + "users.MinimalUserProfile": { + "type": "object", + "properties": { + "active": { + "type": "boolean", + "example": true + }, + "city": { + "type": "string", + "example": "New York" + }, + "country": { + "type": "string", + "example": "US" + }, + "email": { + "type": "string", + "example": "jdoe@gmail.com" + }, + "id": { + "type": "string", + "example": "did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2" + }, + "phoneNumber": { + "type": "string", + "example": "+12099216581" + }, + "pinged": { + "type": "boolean", + "example": false + }, + "profilePictureUrl": { + "type": "string", + "example": "https://somecdn.com/p1.jpg" + }, + "referralType": { + "enum": [ + "CONTACTS", + "T0", + "T1", + "T2" + ], + "allOf": [ + { + "$ref": "#/definitions/users.ReferralType" + } + ], + "example": "T1" + }, + "username": { + "type": "string", + "example": "jdoe" + }, + "verified": { + "type": "boolean", + "example": true + } + } + }, + "users.ReferralAcquisition": { + "type": "object", + "properties": { + "date": { + "type": "string", + "example": "2022-01-03" + }, + "t1": { + "type": "integer", + "example": 22 + }, + "t2": { + "type": "integer", + "example": 13 + } + } + }, + "users.ReferralType": { + "type": "string", + "enum": [ + "CONTACTS", + "T1", + "T2", + "TEAM" + ], + "x-enum-varnames": [ + "ContactsReferrals", + "Tier1Referrals", + "Tier2Referrals", + "TeamReferrals" + ] + }, + "users.Referrals": { + "type": "object", + "properties": { + "active": { + "type": "integer", + "example": 11 + }, + "referrals": { + "type": "array", + "items": { + "$ref": "#/definitions/users.MinimalUserProfile" + } + }, + "total": { + "type": "integer", + "example": 11 + } + } + }, + "users.UserCountTimeSeriesDataPoint": { + "type": "object", + "properties": { + "active": { + "type": "integer", + "example": 11 + }, + "date": { + "type": "string", + "example": "2022-01-03T16:20:52.156534Z" + }, + "total": { + "type": "integer", + "example": 11 + } + } + }, + "users.UserGrowthStatistics": { + "type": "object", + "properties": { + "active": { + "type": "integer", + "example": 11 + }, + "timeSeries": { + "type": "array", + "items": { + "$ref": "#/definitions/users.UserCountTimeSeriesDataPoint" + } + }, + "total": { + "type": "integer", + "example": 11 + } + } } } } \ No newline at end of file diff --git a/cmd/eskimo-hut/api/swagger.yaml b/cmd/eskimo-hut/api/swagger.yaml index 8d800dbc..959622c4 100644 --- a/cmd/eskimo-hut/api/swagger.yaml +++ b/cmd/eskimo-hut/api/swagger.yaml @@ -1,6 +1,5 @@ # SPDX-License-Identifier: ice License 1.0 -basePath: /v1w definitions: main.Auth: properties: @@ -399,6 +398,107 @@ definitions: example: true type: boolean type: object + main.UserProfile: + properties: + agendaPhoneNumberHashes: + example: Ef86A6021afCDe5673511376B2,Ef86A6021afCDe5673511376B2,Ef86A6021afCDe5673511376B2,Ef86A6021afCDe5673511376B2 + type: string + blockchainAccountAddress: + example: 0x4B73C58370AEfcEf86A6021afCDe5673511376B2 + type: string + checksum: + example: "1232412415326543647657" + type: string + city: + example: New York + type: string + clientData: + $ref: '#/definitions/users.JSON' + country: + example: US + type: string + createdAt: + example: "2022-01-03T16:20:52.156534Z" + type: string + email: + example: jdoe@gmail.com + type: string + firstName: + example: John + type: string + hiddenProfileElements: + example: + - level + items: + enum: + - globalRank + - referralCount + - level + - role + - badges + type: string + type: array + id: + example: did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2 + type: string + kycStepBlocked: + allOf: + - $ref: '#/definitions/users.KYCStep' + example: 0 + kycStepPassed: + allOf: + - $ref: '#/definitions/users.KYCStep' + example: 0 + kycStepsCreatedAt: + example: + - "2022-01-03T16:20:52.156534Z" + items: + type: string + type: array + kycStepsLastUpdatedAt: + example: + - "2022-01-03T16:20:52.156534Z" + items: + type: string + type: array + language: + example: en + type: string + lastName: + example: Doe + type: string + miningBlockchainAccountAddress: + example: 0x4B73C58370AEfcEf86A6021afCDe5673511376B2 + type: string + phoneNumber: + example: "+12099216581" + type: string + profilePictureUrl: + example: https://somecdn.com/p1.jpg + type: string + referredBy: + example: did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2 + type: string + repeatableKYCSteps: + additionalProperties: + type: string + type: object + t1ReferralCount: + example: 100 + type: integer + t2ReferralCount: + example: 100 + type: integer + updatedAt: + example: "2022-01-03T16:20:52.156534Z" + type: string + username: + example: jdoe + type: string + verified: + example: true + type: boolean + type: object quiz.Progress: properties: correctAnswers: @@ -520,6 +620,16 @@ definitions: x-enum-varnames: - SuccessVerificationResult - FailureVerificationResult + users.CountryStatistics: + properties: + country: + description: ISO 3166 country code. + example: US + type: string + userCount: + example: 12121212 + type: integer + type: object users.DeviceLocation: properties: city: @@ -558,16 +668,537 @@ definitions: - Social5KYCStep - Social6KYCStep - Social7KYCStep + users.MinimalUserProfile: + properties: + active: + example: true + type: boolean + city: + example: New York + type: string + country: + example: US + type: string + email: + example: jdoe@gmail.com + type: string + id: + example: did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2 + type: string + phoneNumber: + example: "+12099216581" + type: string + pinged: + example: false + type: boolean + profilePictureUrl: + example: https://somecdn.com/p1.jpg + type: string + referralType: + allOf: + - $ref: '#/definitions/users.ReferralType' + enum: + - CONTACTS + - T0 + - T1 + - T2 + example: T1 + username: + example: jdoe + type: string + verified: + example: true + type: boolean + type: object + users.ReferralAcquisition: + properties: + date: + example: "2022-01-03" + type: string + t1: + example: 22 + type: integer + t2: + example: 13 + type: integer + type: object + users.ReferralType: + enum: + - CONTACTS + - T1 + - T2 + - TEAM + type: string + x-enum-varnames: + - ContactsReferrals + - Tier1Referrals + - Tier2Referrals + - TeamReferrals + users.Referrals: + properties: + active: + example: 11 + type: integer + referrals: + items: + $ref: '#/definitions/users.MinimalUserProfile' + type: array + total: + example: 11 + type: integer + type: object + users.UserCountTimeSeriesDataPoint: + properties: + active: + example: 11 + type: integer + date: + example: "2022-01-03T16:20:52.156534Z" + type: string + total: + example: 11 + type: integer + type: object + users.UserGrowthStatistics: + properties: + active: + example: 11 + type: integer + timeSeries: + items: + $ref: '#/definitions/users.UserCountTimeSeriesDataPoint' + type: array + total: + example: 11 + type: integer + type: object info: contact: name: ice.io url: https://ice.io - description: API that handles everything related to write only operations for user's - account, user's devices and statistics about those. + description: API that handles everything related to user's account, user's devices + and statistics about those. title: User Accounts, User Devices, User Statistics API version: latest paths: - /auth/getMetadata: + /v1r/user-statistics/top-countries: + get: + consumes: + - application/json + description: Returns the paginated view of users per country. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - default: + description: Insert your metadata token + in: header + name: X-Account-Metadata + type: string + - description: a keyword to look for in all country codes or names + in: query + name: keyword + type: string + - description: Limit of elements to return. Defaults to 10 + in: query + name: limit + type: integer + - description: Number of elements to skip before collecting elements to return + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/users.CountryStatistics' + type: array + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Statistics + /v1r/user-statistics/user-growth: + get: + consumes: + - application/json + description: Returns statistics about user growth. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - default: + description: Insert your metadata token + in: header + name: X-Account-Metadata + type: string + - description: number of days in the past to look for. Defaults to 3. Max is + 90. + in: query + name: days + type: integer + - description: Timezone in format +04:30 or -03:45 + in: query + name: tz + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/users.UserGrowthStatistics' + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Statistics + /v1r/user-views/username: + get: + consumes: + - application/json + description: Returns public information about an user account based on an username, + making sure the username is valid first. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - default: + description: Insert your metadata token + in: header + name: X-Account-Metadata + type: string + - description: username of the user. It will validate it first + in: query + name: username + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.UserProfile' + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "404": + description: if not found + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Accounts + /v1r/users: + get: + consumes: + - application/json + description: Returns a list of user account based on the provided query parameters. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - default: + description: Insert your metadata token + in: header + name: X-Account-Metadata + type: string + - description: A keyword to look for in the usernames + in: query + name: keyword + required: true + type: string + - description: Limit of elements to return. Defaults to 10 + in: query + name: limit + type: integer + - description: Elements to skip before starting to look for + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/users.MinimalUserProfile' + type: array + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Accounts + /v1r/users/{userId}: + get: + consumes: + - application/json + description: Returns an user's account. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - default: + description: Insert your metadata token + in: header + name: X-Account-Metadata + type: string + - description: ID of the user + in: path + name: userId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.UserProfile' + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "404": + description: if not found + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Accounts + /v1r/users/{userId}/referral-acquisition-history: + get: + consumes: + - application/json + description: Returns the history of referral acquisition for the provided user + id. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - default: + description: Insert your metadata token + in: header + name: X-Account-Metadata + type: string + - description: ID of the user + in: path + name: userId + required: true + type: string + - description: Always is 5, cannot be changed due to DB schema + in: query + name: days + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/users.ReferralAcquisition' + type: array + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "403": + description: if not allowed + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Referrals + /v1r/users/{userId}/referrals: + get: + consumes: + - application/json + description: Returns the referrals of an user. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - default: + description: Insert your metadata token + in: header + name: X-Account-Metadata + type: string + - description: ID of the user + in: path + name: userId + required: true + type: string + - description: 'Type of referrals: `CONTACTS` or `T1` or `T2`' + in: query + name: type + required: true + type: string + - description: Limit of elements to return. Defaults to 10 + in: query + name: limit + type: integer + - description: Number of elements to skip before collecting elements to return + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/users.Referrals' + "400": + description: if validations fail + schema: + $ref: '#/definitions/server.ErrorResponse' + "401": + description: if not authorized + schema: + $ref: '#/definitions/server.ErrorResponse' + "403": + description: if not allowed + schema: + $ref: '#/definitions/server.ErrorResponse' + "422": + description: if syntax fails + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Referrals + /v1w/auth/getMetadata: post: description: Fetches user's metadata based on token's data parameters: @@ -598,7 +1229,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - Auth - /auth/getValidUserForPhoneNumberMigration: + /v1w/auth/getValidUserForPhoneNumberMigration: post: consumes: - application/json @@ -652,7 +1283,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - Auth - /auth/processFaceRecognitionResult: + /v1w/auth/processFaceRecognitionResult: post: consumes: - application/json @@ -713,7 +1344,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - Auth - /auth/refreshTokens: + /v1w/auth/refreshTokens: post: consumes: - application/json @@ -764,7 +1395,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - Auth - /auth/sendSignInLinkToEmail: + /v1w/auth/sendSignInLinkToEmail: post: consumes: - application/json @@ -820,7 +1451,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - Auth - /auth/signInWithConfirmationCode: + /v1w/auth/signInWithConfirmationCode: post: description: Finishes login flow using confirmation code parameters: @@ -859,7 +1490,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - Auth - /kyc/checkKYCStep4Status/users/{userId}: + /v1w/kyc/checkKYCStep4Status/users/{userId}: post: consumes: - application/json @@ -918,7 +1549,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - KYC - /kyc/startOrContinueKYCStep4Session/users/{userId}: + /v1w/kyc/startOrContinueKYCStep4Session/users/{userId}: post: consumes: - application/json @@ -997,7 +1628,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - KYC - /kyc/tryResetKYCSteps/users/{userId}: + /v1w/kyc/tryResetKYCSteps/users/{userId}: post: consumes: - application/json @@ -1064,7 +1695,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - KYC - /kyc/verifySocialKYCStep/users/{userId}: + /v1w/kyc/verifySocialKYCStep/users/{userId}: post: consumes: - application/json @@ -1158,7 +1789,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - KYC - /users: + /v1w/users: post: consumes: - application/json @@ -1223,7 +1854,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - Accounts - /users/{userId}: + /v1w/users/{userId}: delete: consumes: - application/json @@ -1426,7 +2057,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - Accounts - /users/{userId}/devices/{deviceUniqueId}/metadata: + /v1w/users/{userId}/devices/{deviceUniqueId}/metadata: put: consumes: - application/json @@ -1499,7 +2130,7 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - Devices - /users/{userId}/devices/{deviceUniqueId}/metadata/location: + /v1w/users/{userId}/devices/{deviceUniqueId}/metadata/location: put: consumes: - application/json diff --git a/cmd/eskimo-hut/auth.go b/cmd/eskimo-hut/auth.go index 72dd4394..6b0d516a 100644 --- a/cmd/eskimo-hut/auth.go +++ b/cmd/eskimo-hut/auth.go @@ -48,7 +48,7 @@ func (s *service) setupAuthRoutes(router *server.Router) { // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /auth/sendSignInLinkToEmail [POST]. +// @Router /v1w/auth/sendSignInLinkToEmail [POST]. func (s *service) SendSignInLinkToEmail( //nolint:gocritic,funlen // . ctx context.Context, req *server.Request[SendSignInLinkToEmailRequestArg, Auth], @@ -97,7 +97,7 @@ func (s *service) SendSignInLinkToEmail( //nolint:gocritic,funlen // . // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /auth/signInWithConfirmationCode [POST]. +// @Router /v1w/auth/signInWithConfirmationCode [POST]. // //nolint:gocritic,funlen //. func (s *service) SignIn( @@ -156,7 +156,7 @@ func (s *service) SignIn( // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /auth/refreshTokens [POST]. +// @Router /v1w/auth/refreshTokens [POST]. func (s *service) RegenerateTokens( //nolint:gocritic // . ctx context.Context, req *server.Request[RefreshToken, RefreshedToken], @@ -192,7 +192,7 @@ func (s *service) RegenerateTokens( //nolint:gocritic // . // @Failure 404 {object} server.ErrorResponse "if user do not have a metadata yet" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /auth/getMetadata [POST]. +// @Router /v1w/auth/getMetadata [POST]. func (s *service) Metadata( ctx context.Context, req *server.Request[GetMetadataArg, Metadata], @@ -345,7 +345,7 @@ func (s *service) updateMetadataWithFirebaseID( // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /auth/processFaceRecognitionResult [POST]. +// @Router /v1w/auth/processFaceRecognitionResult [POST]. func (s *service) ProcessFaceRecognitionResult( ctx context.Context, req *server.Request[ProcessFaceRecognitionResultArg, any], @@ -435,7 +435,7 @@ func parseProcessFaceRecognitionResultRequest(req *server.Request[ProcessFaceRec // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /auth/getValidUserForPhoneNumberMigration [POST]. +// @Router /v1w/auth/getValidUserForPhoneNumberMigration [POST]. func (s *service) GetValidUserForPhoneNumberMigration( //nolint:funlen,revive // . ctx context.Context, req *server.Request[GetValidUserForPhoneNumberMigrationArg, User], diff --git a/cmd/eskimo-hut/devices.go b/cmd/eskimo-hut/devices.go index 51515c4c..78355fa7 100644 --- a/cmd/eskimo-hut/devices.go +++ b/cmd/eskimo-hut/devices.go @@ -39,7 +39,7 @@ func (s *service) setupDevicesRoutes(router *server.Router) { // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /users/{userId}/devices/{deviceUniqueId}/metadata [PUT]. +// @Router /v1w/users/{userId}/devices/{deviceUniqueId}/metadata [PUT]. func (s *service) ReplaceDeviceMetadata( //nolint:gocritic // False negative. ctx context.Context, req *server.Request[ReplaceDeviceMetadataRequestBody, any], @@ -85,7 +85,7 @@ func (s *service) ReplaceDeviceMetadata( //nolint:gocritic // False negative. // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /users/{userId}/devices/{deviceUniqueId}/metadata/location [PUT]. +// @Router /v1w/users/{userId}/devices/{deviceUniqueId}/metadata/location [PUT]. func (s *service) GetDeviceLocation( //nolint:gocritic // False negative. ctx context.Context, req *server.Request[GetDeviceLocationArg, users.DeviceLocation], diff --git a/cmd/eskimo-hut/eskimo.go b/cmd/eskimo-hut/eskimo.go new file mode 100644 index 00000000..69eede80 --- /dev/null +++ b/cmd/eskimo-hut/eskimo.go @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: ice License 1.0 + +package main + +import ( + "context" + "regexp" + "strings" + stdlibtime "time" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/eskimo/users" + "github.com/ice-blockchain/wintr/server" +) + +// Public API. + +type ( + GetUsersArg struct { + Keyword string `form:"keyword" required:"true" example:"john"` + Limit uint64 `form:"limit" maximum:"1000" example:"10"` // 10 by default. + Offset uint64 `form:"offset" example:"5"` + } + GetUserByIDArg struct { + UserID string `uri:"userId" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + } + GetUserByUsernameArg struct { + Username string `form:"username" required:"true" example:"jdoe"` + } + GetTopCountriesArg struct { + Keyword string `form:"keyword" example:"united states"` + Limit uint64 `form:"limit" maximum:"1000" example:"10"` // 10 by default. + Offset uint64 `form:"offset" example:"5"` + } + GetUserGrowthArg struct { + TZ string `form:"tz" example:"+4:30"` + Days uint64 `form:"days" example:"7"` + } + GetReferralAcquisitionHistoryArg struct { + UserID string `uri:"userId" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + Days uint64 `form:"days" maximum:"30" example:"5"` + } + GetReferralsArg struct { + UserID string `uri:"userId" required:"true" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + Type string `form:"type" required:"true" example:"T1" enums:"T1,T2,CONTACTS"` + Limit uint64 `form:"limit" maximum:"1000" example:"10"` // 10 by default. + Offset uint64 `form:"offset" example:"5"` + } + UserProfile struct { + *users.UserProfile + Checksum string `json:"checksum,omitempty" example:"1232412415326543647657"` + } +) + +// Private API. + +const ( + everythingNotAllowedInUsernameRegex = `[^.a-zA-Z0-9]+` +) + +// Values for server.ErrorResponse#Code. +const ( + invalidKeywordErrorCode = "INVALID_KEYWORD" + + requestingUserIDCtxValueKey = "requestingUserIDCtxValueKey" +) + +// . +var ( + everythingNotAllowedInUsernamePattern = regexp.MustCompile(everythingNotAllowedInUsernameRegex) +) + +func (s *service) registerEskimoRoutes(router *server.Router) { + s.setupUserReadRoutes(router) + s.setupUserReferralRoutes(router) + s.setupUserStatisticsRoutes(router) +} + +func (s *service) setupUserReferralRoutes(router *server.Router) { + router. + Group("v1r"). + GET("users/:userId/referral-acquisition-history", server.RootHandler(s.GetReferralAcquisitionHistory)). + GET("users/:userId/referrals", server.RootHandler(s.GetReferrals)) +} + +// GetReferralAcquisitionHistory godoc +// +// @Schemes +// @Description Returns the history of referral acquisition for the provided user id. +// @Tags Referrals +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param X-Account-Metadata header string false "Insert your metadata token" default() +// @Param userId path string true "ID of the user" +// @Param days query uint64 false "Always is 5, cannot be changed due to DB schema" +// @Success 200 {array} users.ReferralAcquisition +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 403 {object} server.ErrorResponse "if not allowed" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /v1r/users/{userId}/referral-acquisition-history [GET]. +func (s *service) GetReferralAcquisitionHistory( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetReferralAcquisitionHistoryArg, []*users.ReferralAcquisition], +) (*server.Response[[]*users.ReferralAcquisition], *server.Response[server.ErrorResponse]) { + res, err := s.usersProcessor.GetReferralAcquisitionHistory(ctx, req.Data.UserID) + if err != nil { + return nil, server.Unexpected(errors.Wrapf(err, "error getting referral acquisition history for %#v", req.Data)) + } + + return server.OK(&res), nil +} + +// GetReferrals godoc +// +// @Schemes +// @Description Returns the referrals of an user. +// @Tags Referrals +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param X-Account-Metadata header string false "Insert your metadata token" default() +// @Param userId path string true "ID of the user" +// @Param type query string true "Type of referrals: `CONTACTS` or `T1` or `T2`" +// @Param limit query uint64 false "Limit of elements to return. Defaults to 10" +// @Param offset query uint64 false "Number of elements to skip before collecting elements to return" +// @Success 200 {object} users.Referrals +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 403 {object} server.ErrorResponse "if not allowed" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /v1r/users/{userId}/referrals [GET]. +func (s *service) GetReferrals( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetReferralsArg, users.Referrals], +) (*server.Response[users.Referrals], *server.Response[server.ErrorResponse]) { + if req.Data.Limit == 0 { + req.Data.Limit = 10 + } + var validType bool + for _, referralType := range users.ReferralTypes { + if strings.EqualFold(req.Data.Type, string(referralType)) { + validType = true + + break + } + } + if !validType { + err := errors.Errorf("type '%v' is invalid, valid types are %v", req.Data.Type, users.ReferralTypes) + + return nil, server.UnprocessableEntity(err, invalidPropertiesErrorCode) + } + + referrals, err := s.usersProcessor.GetReferrals(ctx, req.Data.UserID, users.ReferralType(strings.ToUpper(req.Data.Type)), req.Data.Limit, req.Data.Offset) + if err != nil { + return nil, server.Unexpected(errors.Wrapf(err, "failed to get referrals for %#v", req.Data)) + } + + return server.OK(referrals), nil +} + +func (s *service) setupUserStatisticsRoutes(router *server.Router) { + router. + Group("v1r"). + GET("user-statistics/top-countries", server.RootHandler(s.GetTopCountries)). + GET("user-statistics/user-growth", server.RootHandler(s.GetUserGrowth)) +} + +// GetTopCountries godoc +// +// @Schemes +// @Description Returns the paginated view of users per country. +// @Tags Statistics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param X-Account-Metadata header string false "Insert your metadata token" default() +// @Param keyword query string false "a keyword to look for in all country codes or names" +// @Param limit query uint64 false "Limit of elements to return. Defaults to 10" +// @Param offset query uint64 false "Number of elements to skip before collecting elements to return" +// @Success 200 {array} users.CountryStatistics +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /v1r/user-statistics/top-countries [GET]. +func (s *service) GetTopCountries( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetTopCountriesArg, []*users.CountryStatistics], +) (*server.Response[[]*users.CountryStatistics], *server.Response[server.ErrorResponse]) { + if req.Data.Limit == 0 { + req.Data.Limit = 10 + } + result, err := s.usersProcessor.GetTopCountries(ctx, req.Data.Keyword, req.Data.Limit, req.Data.Offset) + if err != nil { + return nil, server.Unexpected(errors.Wrapf(err, "failed to get top countries for: %#v", req.Data)) + } + + return server.OK(&result), nil +} + +// GetUserGrowth godoc +// +// @Schemes +// @Description Returns statistics about user growth. +// @Tags Statistics +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param X-Account-Metadata header string false "Insert your metadata token" default() +// @Param days query uint64 false "number of days in the past to look for. Defaults to 3. Max is 90." +// @Param tz query string false "Timezone in format +04:30 or -03:45" +// @Success 200 {object} users.UserGrowthStatistics +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /v1r/user-statistics/user-growth [GET]. +func (s *service) GetUserGrowth( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetUserGrowthArg, users.UserGrowthStatistics], +) (*server.Response[users.UserGrowthStatistics], *server.Response[server.ErrorResponse]) { + const defaultDays, maxDays = 3, 90 + if req.Data.Days == 0 { + req.Data.Days = defaultDays + } + if req.Data.Days > maxDays { + req.Data.Days = maxDays + } + tz := stdlibtime.UTC + if req.Data.TZ != "" { + var invertedTZ string + if req.Data.TZ[0] == '-' { + invertedTZ = "+" + req.Data.TZ[1:] + } else { + invertedTZ = "-" + req.Data.TZ[1:] + } + if t, err := stdlibtime.Parse("-07:00", invertedTZ); err == nil { + tz = t.Location() + } + } + result, err := s.usersProcessor.GetUserGrowth(ctx, req.Data.Days, tz) + if err != nil { + return nil, server.Unexpected(errors.Wrapf(err, "failed to get user growth stats for: %#v", req.Data)) + } + + return server.OK(result), nil +} + +func (s *service) setupUserReadRoutes(router *server.Router) { + router. + Group("v1r"). + GET("users", server.RootHandler(s.GetUsers)). + GET("users/:userId", server.RootHandler(s.GetUserByID)). + GET("user-views/username", server.RootHandler(s.GetUserByUsername)) +} + +// GetUsers godoc +// +// @Schemes +// @Description Returns a list of user account based on the provided query parameters. +// @Tags Accounts +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param X-Account-Metadata header string false "Insert your metadata token" default() +// @Param keyword query string true "A keyword to look for in the usernames" +// @Param limit query uint64 false "Limit of elements to return. Defaults to 10" +// @Param offset query uint64 false "Elements to skip before starting to look for" +// @Success 200 {array} users.MinimalUserProfile +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /v1r/users [GET]. +func (s *service) GetUsers( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetUsersArg, []*users.MinimalUserProfile], +) (*server.Response[[]*users.MinimalUserProfile], *server.Response[server.ErrorResponse]) { + key := string(everythingNotAllowedInUsernamePattern.ReplaceAll([]byte(strings.ToLower(req.Data.Keyword)), []byte(""))) + if key == "" || !strings.EqualFold(key, req.Data.Keyword) { + err := errors.Errorf("username: %v is invalid, it should match regex: %v", req.Data.Keyword, everythingNotAllowedInUsernamePattern) + + return nil, server.BadRequest(err, invalidKeywordErrorCode) + } + if req.Data.Limit == 0 { + req.Data.Limit = 10 + } + resp, err := s.usersProcessor.GetUsers(ctx, req.Data.Keyword, req.Data.Limit, req.Data.Offset) + if err != nil { + return nil, server.Unexpected(errors.Wrapf(err, "failed to get users by %#v", req.Data)) + } + + return server.OK(&resp), nil +} + +// GetUserByID godoc +// +// @Schemes +// @Description Returns an user's account. +// @Tags Accounts +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param X-Account-Metadata header string false "Insert your metadata token" default() +// @Param userId path string true "ID of the user" +// @Success 200 {object} UserProfile +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 404 {object} server.ErrorResponse "if not found" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /v1r/users/{userId} [GET]. +func (s *service) GetUserByID( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetUserByIDArg, UserProfile], +) (*server.Response[UserProfile], *server.Response[server.ErrorResponse]) { + if req.AuthenticatedUser.Role == adminRole && req.Data.UserID != req.AuthenticatedUser.UserID { + ctx = context.WithValue(ctx, requestingUserIDCtxValueKey, req.Data.UserID) //nolint:revive,staticcheck //. + } + usr, err := s.usersProcessor.GetUserByID(ctx, req.Data.UserID) + if err != nil { + if errors.Is(err, users.ErrNotFound) { + return nil, server.NotFound(errors.Wrapf(err, "user with id `%v` was not found", req.Data.UserID), userNotFoundErrorCode) + } + + return nil, server.Unexpected(errors.Wrapf(err, "failed to get user by id: %v", req.Data.UserID)) + } + + return server.OK(&UserProfile{UserProfile: usr, Checksum: usr.Checksum()}), nil +} + +// GetUserByUsername godoc +// +// @Schemes +// @Description Returns public information about an user account based on an username, making sure the username is valid first. +// @Tags Accounts +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param X-Account-Metadata header string false "Insert your metadata token" default() +// @Param username query string true "username of the user. It will validate it first" +// @Success 200 {object} UserProfile +// @Failure 400 {object} server.ErrorResponse "if validations fail" +// @Failure 401 {object} server.ErrorResponse "if not authorized" +// @Failure 404 {object} server.ErrorResponse "if not found" +// @Failure 422 {object} server.ErrorResponse "if syntax fails" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /v1r/user-views/username [GET]. +func (s *service) GetUserByUsername( //nolint:gocritic // False negative. + ctx context.Context, + req *server.Request[GetUserByUsernameArg, UserProfile], +) (*server.Response[UserProfile], *server.Response[server.ErrorResponse]) { + if !users.CompiledUsernameRegex.MatchString(req.Data.Username) { + err := errors.Errorf("username: %v is invalid, it should match regex: %v", req.Data.Username, users.UsernameRegex) + + return nil, server.BadRequest(err, invalidUsernameErrorCode) + } + + resp, err := s.usersProcessor.GetUserByUsername(ctx, strings.ToLower(req.Data.Username)) + if err != nil { + if errors.Is(err, users.ErrNotFound) { + return nil, server.NotFound(errors.Wrapf(err, "user with username `%v` was not found", req.Data.Username), userNotFoundErrorCode) + } + + return nil, server.Unexpected(errors.Wrapf(err, "failed to get user by username: %v", req.Data.Username)) + } + + return server.OK(&UserProfile{UserProfile: resp}), nil +} diff --git a/cmd/eskimo-hut/eskimo_hut.go b/cmd/eskimo-hut/eskimo_hut.go index d2394a7b..193efc45 100644 --- a/cmd/eskimo-hut/eskimo_hut.go +++ b/cmd/eskimo-hut/eskimo_hut.go @@ -22,12 +22,11 @@ import ( // @title User Accounts, User Devices, User Statistics API // @version latest -// @description API that handles everything related to write only operations for user's account, user's devices and statistics about those. +// @description API that handles everything related to user's account, user's devices and statistics about those. // @query.collection.format multi // @schemes https // @contact.name ice.io // @contact.url https://ice.io -// @BasePath /v1w func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -43,6 +42,7 @@ func main() { } func (s *service) RegisterRoutes(router *server.Router) { + s.registerEskimoRoutes(router) s.setupKYCRoutes(router) s.setupUserRoutes(router) s.setupDevicesRoutes(router) diff --git a/cmd/eskimo-hut/kyc.go b/cmd/eskimo-hut/kyc.go index c4bca245..2feb56bd 100644 --- a/cmd/eskimo-hut/kyc.go +++ b/cmd/eskimo-hut/kyc.go @@ -68,7 +68,7 @@ func (s *service) startQuizSession(ctx context.Context, userID users.UserID, lan // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /kyc/startOrContinueKYCStep4Session/users/{userId} [POST]. +// @Router /v1w/kyc/startOrContinueKYCStep4Session/users/{userId} [POST]. func (s *service) StartOrContinueKYCStep4Session( //nolint:gocritic,funlen // . ctx context.Context, req *server.Request[StartOrContinueKYCStep4SessionRequestBody, kycquiz.Quiz], @@ -142,7 +142,7 @@ func (s *service) StartOrContinueKYCStep4Session( //nolint:gocritic,funlen // . // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /kyc/checkKYCStep4Status/users/{userId} [POST]. +// @Router /v1w/kyc/checkKYCStep4Status/users/{userId} [POST]. func (s *service) CheckKYCStep4Status( //nolint:gocritic // . ctx context.Context, req *server.Request[CheckKYCStep4StatusRequestBody, kycquiz.QuizStatus], @@ -181,7 +181,7 @@ func (s *service) CheckKYCStep4Status( //nolint:gocritic // . // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /kyc/verifySocialKYCStep/users/{userId} [POST]. +// @Router /v1w/kyc/verifySocialKYCStep/users/{userId} [POST]. func (s *service) VerifySocialKYCStep( //nolint:gocritic // . ctx context.Context, req *server.Request[kycsocial.VerificationMetadata, kycsocial.Verification], @@ -253,7 +253,7 @@ func validateVerifySocialKYCStep(req *server.Request[kycsocial.VerificationMetad // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /kyc/tryResetKYCSteps/users/{userId} [POST]. +// @Router /v1w/kyc/tryResetKYCSteps/users/{userId} [POST]. func (s *service) TryResetKYCSteps( //nolint:gocritic,funlen,gocognit,revive,cyclop,gocyclo // . ctx context.Context, req *server.Request[TryResetKYCStepsRequestBody, User], diff --git a/cmd/eskimo-hut/users.go b/cmd/eskimo-hut/users.go index f08d90f8..3f9d75ab 100644 --- a/cmd/eskimo-hut/users.go +++ b/cmd/eskimo-hut/users.go @@ -47,7 +47,7 @@ func (s *service) setupUserRoutes(router *server.Router) { // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /users [POST]. +// @Router /v1w/users [POST]. func (s *service) CreateUser( //nolint:funlen,gocritic // . ctx context.Context, req *server.Request[CreateUserRequestBody, User], @@ -136,7 +136,7 @@ func validateCreateUser(req *server.Request[CreateUserRequestBody, User]) *serve // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /users/{userId} [PATCH]. +// @Router /v1w/users/{userId} [PATCH]. func (s *service) ModifyUser( //nolint:gocritic,funlen,revive,cyclop // . ctx context.Context, req *server.Request[ModifyUserRequestBody, ModifyUserResponse], @@ -373,7 +373,7 @@ func verifyPhoneNumberAndUsername(phoneNumber, phoneNumberHash, username string) // @Failure 422 {object} server.ErrorResponse "if syntax fails" // @Failure 500 {object} server.ErrorResponse // @Failure 504 {object} server.ErrorResponse "if request times out" -// @Router /users/{userId} [DELETE]. +// @Router /v1w/users/{userId} [DELETE]. func (s *service) DeleteUser( //nolint:gocritic // False negative. ctx context.Context, req *server.Request[DeleteUserArg, any], diff --git a/go.mod b/go.mod index ff2cc52d..dcf605fc 100644 --- a/go.mod +++ b/go.mod @@ -97,7 +97,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.8 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect diff --git a/go.sum b/go.sum index ec173af6..189a436d 100644 --- a/go.sum +++ b/go.sum @@ -247,8 +247,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=