diff --git a/.env b/.env
index c916afc28..9b48495a2 100644
--- a/.env
+++ b/.env
@@ -47,7 +47,7 @@ MAX_CACHED_ENTITIES=1000
APP_PRIVATE_KEY=this-is-not-so-secret-change-it
SESSION_EXPIRY_AFTER_INACTIVITY_MINUTES=60
SESSION_MAX_DURATION_HOURS=720
-EMAIL_CONFIRMATION_ROUTE=http://localhost:4074/api/v1/confirm-email?token={token}
+EMAIL_CONFIRMATION_ROUTE="http://localhost:4074/api/v1/confirm-email?token={token}&expiry={expiry}&signupType={signupType}"
EMAIL_CONFIRMATION_TOKEN_EXPIRY_TIME_HOURS=24
EMAIL_CONFIRMATION_TOKEN_RATE_LIMIT=5
ACCOUNT_OWNERSHIP_PROOF_EXPIRY_TIME_SECONDS=300 # 5 minutes
@@ -79,6 +79,13 @@ APP_ASSET_STORAGE=https://raw.githubusercontent.com/Joystream/atlas-notification
APP_NAME_ALT=Gleev.xyz
NOTIFICATION_ASSET_ROOT=https://raw.githubusercontent.com/Joystream/atlas-notification-assets/main/icons
+# =====================================================================================
+# Faucet config
+# =====================================================================================
+
+FAUCET_URL=http://localhost:3002/register
+FAUCET_CAPTCHA_BYPASS_KEY=faucet-captcha-bypass-key
+
# =====================================================================================
# Telemetry
# =====================================================================================
diff --git a/.prettierignore b/.prettierignore
index 2c664c4d3..b1bb5ec46 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -10,3 +10,5 @@ db/migrations/*-Views.js
schema.graphql
/scripts/orion-v1-migration/data
/db/export
+src/auth-server/generated
+src/auth-server/emails/templates/preview
diff --git a/db/migrations/1708169663879-Data.js b/db/migrations/1708169663879-Data.js
index 0057bff07..181114344 100644
--- a/db/migrations/1708169663879-Data.js
+++ b/db/migrations/1708169663879-Data.js
@@ -106,14 +106,14 @@ module.exports = class Data1708169663879 {
await db.query(`CREATE INDEX "IDX_4c8f96ccf523e9a3faefd5bdd4" ON "admin"."account" ("email") `)
await db.query(`CREATE INDEX "IDX_601b93655bcbe73cb58d8c80cd" ON "admin"."account" ("membership_id") `)
await db.query(`CREATE INDEX "IDX_df4da05a7a80c1afd18b8f0990" ON "admin"."account" ("joystream_account") `)
- await db.query(`CREATE TABLE "encryption_artifacts" ("id" character varying NOT NULL, "account_id" character varying NOT NULL, "cipher_iv" text NOT NULL, "encrypted_seed" text NOT NULL, CONSTRAINT "EncryptionArtifacts_account" UNIQUE ("account_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_ec8f68a544aadc4fbdadefe4a0" UNIQUE ("account_id"), CONSTRAINT "PK_6441471581ba6d149ad75655bd0" PRIMARY KEY ("id"))`)
- await db.query(`CREATE INDEX "IDX_ec8f68a544aadc4fbdadefe4a0" ON "encryption_artifacts" ("account_id") `)
+ await db.query(`CREATE TABLE "admin"."encryption_artifacts" ("id" character varying NOT NULL, "account_id" character varying NOT NULL, "cipher_iv" text NOT NULL, "encrypted_seed" text NOT NULL, CONSTRAINT "EncryptionArtifacts_account" UNIQUE ("account_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_ec8f68a544aadc4fbdadefe4a0" UNIQUE ("account_id"), CONSTRAINT "PK_6441471581ba6d149ad75655bd0" PRIMARY KEY ("id"))`)
+ await db.query(`CREATE INDEX "IDX_ec8f68a544aadc4fbdadefe4a0" ON "admin"."encryption_artifacts" ("account_id") `)
await db.query(`CREATE TABLE "admin"."session" ("id" character varying NOT NULL, "browser" text NOT NULL, "os" text NOT NULL, "device" text NOT NULL, "device_type" text, "user_id" character varying, "account_id" character varying, "ip" text NOT NULL, "started_at" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_f55da76ac1c3ac420f444d2ff11" PRIMARY KEY ("id"))`)
await db.query(`CREATE INDEX "IDX_30e98e8746699fb9af235410af" ON "admin"."session" ("user_id") `)
await db.query(`CREATE INDEX "IDX_fae5a6b4a57f098e9af8520d49" ON "admin"."session" ("account_id") `)
await db.query(`CREATE INDEX "IDX_213b5a19bfdbe0ab6e06b1dede" ON "admin"."session" ("ip") `)
- await db.query(`CREATE TABLE "session_encryption_artifacts" ("id" character varying NOT NULL, "session_id" character varying NOT NULL, "cipher_iv" text NOT NULL, "cipher_key" text NOT NULL, CONSTRAINT "SessionEncryptionArtifacts_session" UNIQUE ("session_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_3612880efd8926a17eba5ab0e1" UNIQUE ("session_id"), CONSTRAINT "PK_e328da2643599e265a848219885" PRIMARY KEY ("id"))`)
- await db.query(`CREATE INDEX "IDX_3612880efd8926a17eba5ab0e1" ON "session_encryption_artifacts" ("session_id") `)
+ await db.query(`CREATE TABLE "admin"."session_encryption_artifacts" ("id" character varying NOT NULL, "session_id" character varying NOT NULL, "cipher_iv" text NOT NULL, "cipher_key" text NOT NULL, CONSTRAINT "SessionEncryptionArtifacts_session" UNIQUE ("session_id") DEFERRABLE INITIALLY DEFERRED, CONSTRAINT "REL_3612880efd8926a17eba5ab0e1" UNIQUE ("session_id"), CONSTRAINT "PK_e328da2643599e265a848219885" PRIMARY KEY ("id"))`)
+ await db.query(`CREATE INDEX "IDX_3612880efd8926a17eba5ab0e1" ON "admin"."session_encryption_artifacts" ("session_id") `)
await db.query(`CREATE TABLE "admin"."token" ("id" character varying NOT NULL, "type" character varying(18) NOT NULL, "issued_at" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry" TIMESTAMP WITH TIME ZONE NOT NULL, "issued_for_id" character varying, CONSTRAINT "PK_82fae97f905930df5d62a702fc9" PRIMARY KEY ("id"))`)
await db.query(`CREATE INDEX "IDX_a6fe18c105f85a63d761ccb078" ON "admin"."token" ("issued_for_id") `)
await db.query(`CREATE TABLE "admin"."nft_history_entry" ("id" character varying NOT NULL, "nft_id" character varying, "event_id" character varying, CONSTRAINT "PK_9018e80b335a965a54959c4c6e2" PRIMARY KEY ("id"))`)
@@ -204,10 +204,10 @@ module.exports = class Data1708169663879 {
await db.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_122be1f0696e0255acf95f9e336" FOREIGN KEY ("event_id") REFERENCES "admin"."event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
await db.query(`ALTER TABLE "admin"."account" ADD CONSTRAINT "FK_efef1e5fdbe318a379c06678c51" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
await db.query(`ALTER TABLE "admin"."account" ADD CONSTRAINT "FK_601b93655bcbe73cb58d8c80cd3" FOREIGN KEY ("membership_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
- await db.query(`ALTER TABLE "encryption_artifacts" ADD CONSTRAINT "FK_ec8f68a544aadc4fbdadefe4a0a" FOREIGN KEY ("account_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+ await db.query(`ALTER TABLE "admin"."encryption_artifacts" ADD CONSTRAINT "FK_ec8f68a544aadc4fbdadefe4a0a" FOREIGN KEY ("account_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
await db.query(`ALTER TABLE "admin"."session" ADD CONSTRAINT "FK_30e98e8746699fb9af235410aff" FOREIGN KEY ("user_id") REFERENCES "admin"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
await db.query(`ALTER TABLE "admin"."session" ADD CONSTRAINT "FK_fae5a6b4a57f098e9af8520d499" FOREIGN KEY ("account_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
- await db.query(`ALTER TABLE "session_encryption_artifacts" ADD CONSTRAINT "FK_3612880efd8926a17eba5ab0e1a" FOREIGN KEY ("session_id") REFERENCES "admin"."session"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+ await db.query(`ALTER TABLE "admin"."session_encryption_artifacts" ADD CONSTRAINT "FK_3612880efd8926a17eba5ab0e1a" FOREIGN KEY ("session_id") REFERENCES "admin"."session"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
await db.query(`ALTER TABLE "admin"."token" ADD CONSTRAINT "FK_a6fe18c105f85a63d761ccb0780" FOREIGN KEY ("issued_for_id") REFERENCES "admin"."account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
await db.query(`ALTER TABLE "admin"."nft_history_entry" ADD CONSTRAINT "FK_57f51d35ecab042478fe2e31c19" FOREIGN KEY ("nft_id") REFERENCES "admin"."owned_nft"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
await db.query(`ALTER TABLE "admin"."nft_history_entry" ADD CONSTRAINT "FK_d1a28b178f5d028d048d40ce208" FOREIGN KEY ("event_id") REFERENCES "admin"."event"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
@@ -331,13 +331,13 @@ module.exports = class Data1708169663879 {
await db.query(`DROP INDEX "admin"."IDX_4c8f96ccf523e9a3faefd5bdd4"`)
await db.query(`DROP INDEX "admin"."IDX_601b93655bcbe73cb58d8c80cd"`)
await db.query(`DROP INDEX "admin"."IDX_df4da05a7a80c1afd18b8f0990"`)
- await db.query(`DROP TABLE "encryption_artifacts"`)
+ await db.query(`DROP TABLE "admin"."encryption_artifacts"`)
await db.query(`DROP INDEX "public"."IDX_ec8f68a544aadc4fbdadefe4a0"`)
await db.query(`DROP TABLE "admin"."session"`)
await db.query(`DROP INDEX "admin"."IDX_30e98e8746699fb9af235410af"`)
await db.query(`DROP INDEX "admin"."IDX_fae5a6b4a57f098e9af8520d49"`)
await db.query(`DROP INDEX "admin"."IDX_213b5a19bfdbe0ab6e06b1dede"`)
- await db.query(`DROP TABLE "session_encryption_artifacts"`)
+ await db.query(`DROP TABLE "admin"."session_encryption_artifacts"`)
await db.query(`DROP INDEX "public"."IDX_3612880efd8926a17eba5ab0e1"`)
await db.query(`DROP TABLE "admin"."token"`)
await db.query(`DROP INDEX "admin"."IDX_a6fe18c105f85a63d761ccb078"`)
@@ -429,10 +429,10 @@ module.exports = class Data1708169663879 {
await db.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_122be1f0696e0255acf95f9e336"`)
await db.query(`ALTER TABLE "admin"."account" DROP CONSTRAINT "FK_efef1e5fdbe318a379c06678c51"`)
await db.query(`ALTER TABLE "admin"."account" DROP CONSTRAINT "FK_601b93655bcbe73cb58d8c80cd3"`)
- await db.query(`ALTER TABLE "encryption_artifacts" DROP CONSTRAINT "FK_ec8f68a544aadc4fbdadefe4a0a"`)
+ await db.query(`ALTER TABLE "admin"."encryption_artifacts" DROP CONSTRAINT "FK_ec8f68a544aadc4fbdadefe4a0a"`)
await db.query(`ALTER TABLE "admin"."session" DROP CONSTRAINT "FK_30e98e8746699fb9af235410aff"`)
await db.query(`ALTER TABLE "admin"."session" DROP CONSTRAINT "FK_fae5a6b4a57f098e9af8520d499"`)
- await db.query(`ALTER TABLE "session_encryption_artifacts" DROP CONSTRAINT "FK_3612880efd8926a17eba5ab0e1a"`)
+ await db.query(`ALTER TABLE "admin"."session_encryption_artifacts" DROP CONSTRAINT "FK_3612880efd8926a17eba5ab0e1a"`)
await db.query(`ALTER TABLE "admin"."token" DROP CONSTRAINT "FK_a6fe18c105f85a63d761ccb0780"`)
await db.query(`ALTER TABLE "admin"."nft_history_entry" DROP CONSTRAINT "FK_57f51d35ecab042478fe2e31c19"`)
await db.query(`ALTER TABLE "admin"."nft_history_entry" DROP CONSTRAINT "FK_d1a28b178f5d028d048d40ce208"`)
diff --git a/db/migrations/1722676430400-Data.js b/db/migrations/1722676430400-Data.js
new file mode 100644
index 000000000..a9d6704bd
--- /dev/null
+++ b/db/migrations/1722676430400-Data.js
@@ -0,0 +1,49 @@
+module.exports = class Data1722676430400 {
+ name = 'Data1722676430400'
+
+ async up(db) {
+ await db.query(`ALTER TABLE "admin"."account" DROP CONSTRAINT "FK_601b93655bcbe73cb58d8c80cd3"`)
+ await db.query(`DROP INDEX "admin"."IDX_601b93655bcbe73cb58d8c80cd"`)
+ await db.query(`DROP INDEX "admin"."IDX_df4da05a7a80c1afd18b8f0990"`)
+ await db.query(`ALTER TABLE "membership" RENAME COLUMN "controller_account" TO "controller_account_id"`)
+ await db.query(`CREATE TABLE "blockchain_account" ("id" character varying NOT NULL, CONSTRAINT "PK_3d07d692a436bc34ef4093d9c60" PRIMARY KEY ("id"))`)
+ await db.query(`CREATE TABLE "admin"."email_confirmation_token" ("id" character varying NOT NULL, "issued_at" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry" TIMESTAMP WITH TIME ZONE NOT NULL, "email" text NOT NULL, CONSTRAINT "PK_2fa8d5586af7e96201b84492131" PRIMARY KEY ("id"))`)
+ await db.query(`ALTER TABLE "admin"."account" DROP COLUMN "is_email_confirmed"`)
+ await db.query(`ALTER TABLE "admin"."account" DROP CONSTRAINT "Account_membership"`)
+ await db.query(`ALTER TABLE "admin"."account" DROP COLUMN "membership_id"`)
+ await db.query(`ALTER TABLE "admin"."account" DROP CONSTRAINT "Account_joystreamAccount"`)
+ await db.query(`ALTER TABLE "admin"."account" DROP COLUMN "joystream_account"`)
+ await db.query(`ALTER TABLE "admin"."account" DROP COLUMN "referrer_channel_id"`)
+ await db.query(`ALTER TABLE "admin"."account" ADD "joystream_account_id" character varying NOT NULL`)
+ await db.query(`ALTER TABLE "admin"."account" ADD CONSTRAINT "UQ_90debbc4217372d2464201c576a" UNIQUE ("joystream_account_id")`)
+ await db.query(`ALTER TABLE "membership" DROP COLUMN "controller_account_id"`)
+ await db.query(`ALTER TABLE "membership" ADD "controller_account_id" character varying`)
+ await db.query(`CREATE INDEX "IDX_58492b909a36e6a3e4dabd4674" ON "membership" ("controller_account_id") `)
+ await db.query(`CREATE INDEX "IDX_90debbc4217372d2464201c576" ON "admin"."account" ("joystream_account_id") `)
+ await db.query(`ALTER TABLE "membership" ADD CONSTRAINT "FK_58492b909a36e6a3e4dabd46743" FOREIGN KEY ("controller_account_id") REFERENCES "blockchain_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+ await db.query(`ALTER TABLE "admin"."account" ADD CONSTRAINT "FK_90debbc4217372d2464201c576a" FOREIGN KEY ("joystream_account_id") REFERENCES "blockchain_account"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+ }
+
+ async down(db) {
+ await db.query(`ALTER TABLE "admin"."account" ADD CONSTRAINT "FK_601b93655bcbe73cb58d8c80cd3" FOREIGN KEY ("membership_id") REFERENCES "membership"("id") ON DELETE NO ACTION ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`)
+ await db.query(`CREATE INDEX "IDX_601b93655bcbe73cb58d8c80cd" ON "admin"."account" ("membership_id") `)
+ await db.query(`CREATE INDEX "IDX_df4da05a7a80c1afd18b8f0990" ON "admin"."account" ("joystream_account") `)
+ await db.query(`ALTER TABLE "membership" RENAME COLUMN "controller_account_id" TO "controller_account"`)
+ await db.query(`DROP TABLE "blockchain_account"`)
+ await db.query(`DROP TABLE "admin"."email_confirmation_token"`)
+ await db.query(`ALTER TABLE "admin"."account" ADD "is_email_confirmed" boolean NOT NULL`)
+ await db.query(`ALTER TABLE "admin"."account" ADD CONSTRAINT "Account_membership" UNIQUE ("membership_id")`)
+ await db.query(`ALTER TABLE "admin"."account" ADD "membership_id" character varying NOT NULL`)
+ await db.query(`ALTER TABLE "admin"."account" ADD CONSTRAINT "Account_joystreamAccount" UNIQUE ("joystream_account")`)
+ await db.query(`ALTER TABLE "admin"."account" ADD "joystream_account" text NOT NULL`)
+ await db.query(`ALTER TABLE "admin"."account" ADD "referrer_channel_id" text`)
+ await db.query(`ALTER TABLE "admin"."account" DROP COLUMN "joystream_account_id"`)
+ await db.query(`ALTER TABLE "admin"."account" DROP CONSTRAINT "UQ_90debbc4217372d2464201c576a"`)
+ await db.query(`ALTER TABLE "membership" ADD "controller_account_id" text NOT NULL`)
+ await db.query(`ALTER TABLE "membership" DROP COLUMN "controller_account_id"`)
+ await db.query(`DROP INDEX "public"."IDX_58492b909a36e6a3e4dabd4674"`)
+ await db.query(`DROP INDEX "admin"."IDX_90debbc4217372d2464201c576"`)
+ await db.query(`ALTER TABLE "membership" DROP CONSTRAINT "FK_58492b909a36e6a3e4dabd46743"`)
+ await db.query(`ALTER TABLE "admin"."account" DROP CONSTRAINT "FK_90debbc4217372d2464201c576a"`)
+ }
+}
diff --git a/db/migrations/1721141313757-Views.js b/db/migrations/1722676430521-Views.js
similarity index 94%
rename from db/migrations/1721141313757-Views.js
rename to db/migrations/1722676430521-Views.js
index dac2d967e..8a7854642 100644
--- a/db/migrations/1721141313757-Views.js
+++ b/db/migrations/1722676430521-Views.js
@@ -1,8 +1,8 @@
const { getViewDefinitions } = require('../viewDefinitions')
-module.exports = class Views1721141313757 {
- name = 'Views1721141313757'
+module.exports = class Views1722676430521 {
+ name = 'Views1722676430521'
async up(db) {
// these two queries will be invoked and the cleaned up by the squid itself
diff --git a/docker.env b/docker.env
index 3b0da8318..caebf25d0 100644
--- a/docker.env
+++ b/docker.env
@@ -7,3 +7,4 @@ DB_HOST=orion_db
PROCESSOR_HOST=orion_processor
# Archive gateway host&port (can be overriden via local env)
ARCHIVE_GATEWAY_URL=${CUSTOM_ARCHIVE_GATEWAY_URL:-http://orion_archive_gateway:8000/graphql}
+# ARCHIVE_GATEWAY_URL=${CUSTOM_ARCHIVE_GATEWAY_URL:-https://archive.joystream.org/graphql}
diff --git a/package-lock.json b/package-lock.json
index 58a663901..0b223d258 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "orion",
- "version": "4.0.6",
+ "version": "5.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "orion",
- "version": "4.0.6",
+ "version": "5.0.0",
"hasInstallScript": true,
"workspaces": [
"network-tests"
diff --git a/package.json b/package.json
index b70db4004..637d263e6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "orion",
- "version": "4.0.6",
+ "version": "5.0.0",
"engines": {
"node": ">=16"
},
diff --git a/schema/auth.graphql b/schema/auth.graphql
index b87d6c99a..4941994d6 100644
--- a/schema/auth.graphql
+++ b/schema/auth.graphql
@@ -41,7 +41,7 @@ type User @entity @schema(name: "admin") {
nftFeaturingRequests: [NftFeaturingRequest!]! @derivedFrom(field: "user")
}
-type EncryptionArtifacts @entity {
+type EncryptionArtifacts @entity @schema(name: "admin") {
"ID / lookupKey"
id: ID!
@@ -55,7 +55,7 @@ type EncryptionArtifacts @entity {
encryptedSeed: String!
}
-type SessionEncryptionArtifacts @entity {
+type SessionEncryptionArtifacts @entity @schema(name: "admin") {
"Unique identifier"
id: ID!
@@ -112,31 +112,20 @@ type Account @entity @schema(name: "admin") {
"Gateway account's e-mail address"
email: String! @unique
- """
- Indicates whether the gateway account's e-mail has been confirmed or not.
- """
- isEmailConfirmed: Boolean!
-
"Indicates whether the access to the gateway account is blocked"
isBlocked: Boolean!
"Time when the gateway account was registered"
registeredAt: DateTime!
- "On-chain membership associated with the gateway account"
- membership: Membership! @unique
-
"Blockchain (joystream) account associated with the gateway account"
- joystreamAccount: String! @unique
+ joystreamAccount: BlockchainAccount! @unique
"runtime notifications"
notifications: [Notification!]! @derivedFrom(field: "account")
"notification preferences for the account"
notificationPreferences: AccountNotificationPreferences!
-
- "ID of the channel which referred the user to the platform"
- referrerChannelId: String
}
type AccountNotificationPreferences {
@@ -214,3 +203,26 @@ type Token @entity @schema(name: "admin") {
"The account the token was issued for"
issuedFor: Account!
}
+
+type EmailConfirmationToken @entity @schema(name: "admin") {
+ "The token itself (32-byte string, securely random)"
+ id: ID!
+
+ "When was the token issued"
+ issuedAt: DateTime!
+
+ "When does the token expire or when has it expired"
+ expiry: DateTime!
+
+ # "The User the token was issued for"
+ # issuedFor: User!
+
+ "The email the token was issued for"
+ email: String!
+
+ # "Indicates whether the token has been confirmed or not"
+ # isConfirmed: Boolean!
+
+ # "Time when the token was confirmed"
+ # confirmedAt: DateTime
+}
diff --git a/schema/membership.graphql b/schema/membership.graphql
index 91d3d80ba..3e3bc0175 100644
--- a/schema/membership.graphql
+++ b/schema/membership.graphql
@@ -8,6 +8,14 @@ type AvatarUri @variant {
avatarUri: String!
}
+type BlockchainAccount @entity {
+ "The blockchain account id/address"
+ id: ID!
+
+ "Membership associated with the blockchain account (controllerAccount)"
+ memberships: [Membership!] @derivedFrom(field: "controllerAccount")
+}
+
union Avatar = AvatarObject | AvatarUri
type MemberMetadata @entity {
@@ -41,7 +49,7 @@ type Membership @entity {
metadata: MemberMetadata @derivedFrom(field: "member")
"Member's controller account id"
- controllerAccount: String!
+ controllerAccount: BlockchainAccount!
"Auctions in which is this user whitelisted to participate"
whitelistedInAuctions: [AuctionWhitelistedMember!] @derivedFrom(field: "member")
diff --git a/src/auth-server/docs/.openapi-generator/FILES b/src/auth-server/docs/.openapi-generator/FILES
index 857a276bf..1f14f057c 100644
--- a/src/auth-server/docs/.openapi-generator/FILES
+++ b/src/auth-server/docs/.openapi-generator/FILES
@@ -6,7 +6,6 @@ Models/AnonymousUserAuthResponseData.md
Models/AnonymousUserAuthResponseData_allOf.md
Models/ChangeAccountRequestData.md
Models/ChangeAccountRequestData_allOf.md
-Models/ConfirmEmailRequestData.md
Models/CreateAccountRequestData.md
Models/CreateAccountRequestData_allOf.md
Models/EncryptionArtifacts.md
diff --git a/src/auth-server/docs/Apis/DefaultApi.md b/src/auth-server/docs/Apis/DefaultApi.md
index 6184e76e2..141aa93a3 100644
--- a/src/auth-server/docs/Apis/DefaultApi.md
+++ b/src/auth-server/docs/Apis/DefaultApi.md
@@ -6,7 +6,6 @@ All URIs are relative to *http://localhost/api/v1*
|------------- | ------------- | -------------|
| [**anonymousAuth**](DefaultApi.md#anonymousAuth) | **POST** /anonymous-auth | |
| [**changeAccount**](DefaultApi.md#changeAccount) | **POST** /change-account | |
-| [**confirmEmail**](DefaultApi.md#confirmEmail) | **POST** /confirm-email | |
| [**createAccount**](DefaultApi.md#createAccount) | **POST** /account | |
| [**getArtifacts**](DefaultApi.md#getArtifacts) | **GET** /artifacts | |
| [**getSessionArtifacts**](DefaultApi.md#getSessionArtifacts) | **GET** /session-artifacts | |
@@ -71,33 +70,6 @@ No authorization required
- **Content-Type**: application/json
- **Accept**: application/json
-
-# **confirmEmail**
-> GenericOkResponseData confirmEmail(ConfirmEmailRequestData)
-
-
-
- Confirm account's e-mail address provided during registration.
-
-### Parameters
-
-|Name | Type | Description | Notes |
-|------------- | ------------- | ------------- | -------------|
-| **ConfirmEmailRequestData** | [**ConfirmEmailRequestData**](../Models/ConfirmEmailRequestData.md)| | [optional] |
-
-### Return type
-
-[**GenericOkResponseData**](../Models/GenericOkResponseData.md)
-
-### Authorization
-
-No authorization required
-
-### HTTP request headers
-
-- **Content-Type**: application/json
-- **Accept**: application/json
-
# **createAccount**
> GenericOkResponseData createAccount(CreateAccountRequestData)
@@ -288,7 +260,7 @@ This endpoint does not need any parameter.
- Request a token to be sent to account's e-mail address, which will allow confirming the ownership of the e-mail by the user.
+ Request a token to be sent to e-mail address (as the first step of signup process), which will allow confirming the ownership of the e-mail by the user.
### Parameters
@@ -302,7 +274,7 @@ This endpoint does not need any parameter.
### Authorization
-No authorization required
+[cookieAuth](../README.md#cookieAuth)
### HTTP request headers
diff --git a/src/auth-server/docs/Models/ConfirmEmailRequestData.md b/src/auth-server/docs/Models/ConfirmEmailRequestData.md
deleted file mode 100644
index 3d3542cb7..000000000
--- a/src/auth-server/docs/Models/ConfirmEmailRequestData.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# ConfirmEmailRequestData
-## Properties
-
-| Name | Type | Description | Notes |
-|------------ | ------------- | ------------- | -------------|
-| **token** | **String** | Confirmation token recieved by the user via an e-mail. | [default to null] |
-
-[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
-
diff --git a/src/auth-server/docs/Models/RequestTokenRequestData.md b/src/auth-server/docs/Models/RequestTokenRequestData.md
index 8548ae512..0219eec2f 100644
--- a/src/auth-server/docs/Models/RequestTokenRequestData.md
+++ b/src/auth-server/docs/Models/RequestTokenRequestData.md
@@ -4,6 +4,7 @@
| Name | Type | Description | Notes |
|------------ | ------------- | ------------- | -------------|
| **email** | **String** | User's e-mail address. | [default to null] |
+| **signupType** | **String** | Signup type. | [default to null] |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
diff --git a/src/auth-server/docs/README.md b/src/auth-server/docs/README.md
index 859c73964..3db6b8e5a 100644
--- a/src/auth-server/docs/README.md
+++ b/src/auth-server/docs/README.md
@@ -9,7 +9,6 @@ All URIs are relative to *http://localhost/api/v1*
|------------ | ------------- | ------------- | -------------|
| *DefaultApi* | [**anonymousAuth**](Apis/DefaultApi.md#anonymousauth) | **POST** /anonymous-auth | Authenticate as an anonymous user, either using an existing user identifier or creating a new one. |
*DefaultApi* | [**changeAccount**](Apis/DefaultApi.md#changeaccount) | **POST** /change-account | Change the blockchain (Joystream) account associated with the Gateway account. Delete the old account's encryption artifacts and optionally set new ones. |
-*DefaultApi* | [**confirmEmail**](Apis/DefaultApi.md#confirmemail) | **POST** /confirm-email | Confirm account's e-mail address provided during registration. |
*DefaultApi* | [**createAccount**](Apis/DefaultApi.md#createaccount) | **POST** /account | Create a new Gateway account. Requires anonymousAuth to be performed first. |
*DefaultApi* | [**getArtifacts**](Apis/DefaultApi.md#getartifacts) | **GET** /artifacts | Get wallet seed encryption artifacts. |
*DefaultApi* | [**getSessionArtifacts**](Apis/DefaultApi.md#getsessionartifacts) | **GET** /session-artifacts | Get wallet seed encryption artifacts for the current session. |
@@ -17,7 +16,7 @@ All URIs are relative to *http://localhost/api/v1*
*DefaultApi* | [**logout**](Apis/DefaultApi.md#logout) | **POST** /logout | Terminate the current session. |
*DefaultApi* | [**postSessionArtifacts**](Apis/DefaultApi.md#postsessionartifacts) | **POST** /session-artifacts | Save wallet seed encryption artifacts for the current session on the server. |
*DefaultApi* | [**registerUserInteraction**](Apis/DefaultApi.md#registeruserinteraction) | **POST** /register-user-interaction | Register a user interaction with Atlas part. |
-*DefaultApi* | [**requestEmailConfirmationToken**](Apis/DefaultApi.md#requestemailconfirmationtoken) | **POST** /request-email-confirmation-token | Request a token to be sent to account's e-mail address, which will allow confirming the ownership of the e-mail by the user. |
+*DefaultApi* | [**requestEmailConfirmationToken**](Apis/DefaultApi.md#requestemailconfirmationtoken) | **POST** /request-email-confirmation-token | Request a token to be sent to e-mail address (as the first step of signup process), which will allow confirming the ownership of the e-mail by the user. |
@@ -30,7 +29,6 @@ All URIs are relative to *http://localhost/api/v1*
- [AnonymousUserAuthResponseData_allOf](./Models/AnonymousUserAuthResponseData_allOf.md)
- [ChangeAccountRequestData](./Models/ChangeAccountRequestData.md)
- [ChangeAccountRequestData_allOf](./Models/ChangeAccountRequestData_allOf.md)
- - [ConfirmEmailRequestData](./Models/ConfirmEmailRequestData.md)
- [CreateAccountRequestData](./Models/CreateAccountRequestData.md)
- [CreateAccountRequestData_allOf](./Models/CreateAccountRequestData_allOf.md)
- [EncryptionArtifacts](./Models/EncryptionArtifacts.md)
diff --git a/src/auth-server/emails/index.ts b/src/auth-server/emails/index.ts
index 3fce25be5..10e6a5e54 100644
--- a/src/auth-server/emails/index.ts
+++ b/src/auth-server/emails/index.ts
@@ -1,5 +1,5 @@
-import { compile } from 'handlebars'
import fs from 'fs'
+import { compile } from 'handlebars'
import path from 'path'
import { NotificationData } from '../../utils/notification/notificationsData'
@@ -14,7 +14,6 @@ function getEmailTemplateData(templatePath: string): (data: T) => string {
// type aliases for template data
type RegisterEmailTemplateData = {
link: string
- linkExpiryDate: string
appName: string
}
diff --git a/src/auth-server/generated/api-types.ts b/src/auth-server/generated/api-types.ts
index 8fd15be81..83da9804f 100644
--- a/src/auth-server/generated/api-types.ts
+++ b/src/auth-server/generated/api-types.ts
@@ -3,432 +3,403 @@
* Do not make direct changes to the file.
*/
+
export interface paths {
- '/register-user-interaction': {
+ "/register-user-interaction": {
/** @description Register a user interaction with Atlas part. */
- post: operations['registerUserInteraction']
- }
- '/anonymous-auth': {
+ post: operations["registerUserInteraction"];
+ };
+ "/anonymous-auth": {
/** @description Authenticate as an anonymous user, either using an existing user identifier or creating a new one. */
- post: operations['anonymousAuth']
- }
- '/login': {
+ post: operations["anonymousAuth"];
+ };
+ "/login": {
/** @description Login to user's account by providing a message signed by the associated blockchain account. */
- post: operations['login']
- }
- '/artifacts': {
+ post: operations["login"];
+ };
+ "/artifacts": {
/** @description Get wallet seed encryption artifacts. */
- get: operations['getArtifacts']
- }
- '/session-artifacts': {
+ get: operations["getArtifacts"];
+ };
+ "/session-artifacts": {
/** @description Get wallet seed encryption artifacts for the current session. */
- get: operations['getSessionArtifacts']
+ get: operations["getSessionArtifacts"];
/** @description Save wallet seed encryption artifacts for the current session on the server. */
- post: operations['postSessionArtifacts']
- }
- '/account': {
+ post: operations["postSessionArtifacts"];
+ };
+ "/account": {
/** @description Create a new Gateway account. Requires anonymousAuth to be performed first. */
- post: operations['createAccount']
- }
- '/confirm-email': {
- /** @description Confirm account's e-mail address provided during registration. */
- post: operations['confirmEmail']
- }
- '/request-email-confirmation-token': {
- /** @description Request a token to be sent to account's e-mail address, which will allow confirming the ownership of the e-mail by the user. */
- post: operations['requestEmailConfirmationToken']
- }
- '/change-account': {
+ post: operations["createAccount"];
+ };
+ "/request-email-confirmation-token": {
+ /** @description Request a token to be sent to e-mail address (as the first step of signup process), which will allow confirming the ownership of the e-mail by the user. */
+ post: operations["requestEmailConfirmationToken"];
+ };
+ "/change-account": {
/** @description Change the blockchain (Joystream) account associated with the Gateway account. Delete the old account's encryption artifacts and optionally set new ones. */
- post: operations['changeAccount']
- }
- '/logout': {
+ post: operations["changeAccount"];
+ };
+ "/logout": {
/** @description Terminate the current session. */
- post: operations['logout']
- }
+ post: operations["logout"];
+ };
}
-export type webhooks = Record
+export type webhooks = Record;
export interface components {
schemas: {
ActionExecutionPayload: {
- joystreamAccountId: string
- gatewayName: string
- timestamp: number
- action: string
- }
+ joystreamAccountId: string;
+ gatewayName: string;
+ timestamp: number;
+ action: string;
+ };
ActionExecutionRequestData: {
- signature: string
- payload: components['schemas']['ActionExecutionPayload']
- }
+ signature: string;
+ payload: components["schemas"]["ActionExecutionPayload"];
+ };
RegisterUserInteractionRequestData: {
- entityId: string
- type: string
- }
+ entityId: string;
+ type: string;
+ };
AnonymousUserAuthRequestData: {
- userId?: string
- }
- AnonymousUserAuthResponseData: components['schemas']['GenericOkResponseData'] & {
- userId: string
- }
- LoginRequestData: components['schemas']['ActionExecutionRequestData'] & {
- payload?: components['schemas']['ActionExecutionPayload'] & {
+ userId?: string;
+ };
+ AnonymousUserAuthResponseData: components["schemas"]["GenericOkResponseData"] & {
+ userId: string;
+ };
+ LoginRequestData: components["schemas"]["ActionExecutionRequestData"] & ({
+ payload?: components["schemas"]["ActionExecutionPayload"] & {
/** @enum {string} */
- action?: 'login'
- }
- }
+ action?: "login";
+ };
+ });
LoginResponseData: {
- accountId: string
- }
- CreateAccountRequestData: components['schemas']['ActionExecutionRequestData'] & {
- payload?: components['schemas']['ActionExecutionPayload'] & {
+ accountId: string;
+ };
+ CreateAccountRequestData: components["schemas"]["ActionExecutionRequestData"] & ({
+ payload?: components["schemas"]["ActionExecutionPayload"] & {
/** @enum {string} */
- action?: 'createAccount'
- memberId: string
- email: string
- encryptionArtifacts?: components['schemas']['EncryptionArtifacts']
- }
- }
- ChangeAccountRequestData: components['schemas']['ActionExecutionRequestData'] & {
- payload?: components['schemas']['ActionExecutionPayload'] & {
+ action?: "createAccount";
+ email: string;
+ emailConfirmationToken?: string;
+ encryptionArtifacts?: components["schemas"]["EncryptionArtifacts"];
+ };
+ });
+ ChangeAccountRequestData: components["schemas"]["ActionExecutionRequestData"] & ({
+ payload?: components["schemas"]["ActionExecutionPayload"] & {
/** @enum {string} */
- action?: 'changeAccount'
- gatewayAccountId: string
- newArtifacts?: components['schemas']['EncryptionArtifacts']
- }
- }
- ConfirmEmailRequestData: {
- /** @description Confirmation token recieved by the user via an e-mail. */
- token: string
- }
+ action?: "changeAccount";
+ gatewayAccountId: string;
+ newArtifacts?: components["schemas"]["EncryptionArtifacts"];
+ };
+ });
RequestTokenRequestData: {
/** @description User's e-mail address. */
- email: string
- }
+ email: string;
+ /**
+ * @description Signup type.
+ * @enum {string}
+ */
+ signupType: "internal" | "external" | "ypp";
+ };
GenericErrorResponseData: {
- message?: string
- errors?: string[]
- }
+ message?: string;
+ errors?: string[];
+ };
GenericOkResponseData: {
- success: boolean
- }
+ success: boolean;
+ };
EncryptionArtifacts: {
- id: string
- encryptedSeed: string
- cipherIv: string
- }
+ id: string;
+ encryptedSeed: string;
+ cipherIv: string;
+ };
SessionEncryptionArtifacts: {
- cipherKey: string
- cipherIv: string
- }
- }
+ cipherKey: string;
+ cipherIv: string;
+ };
+ };
responses: {
/** @description Ok */
AnonymousUserAuthOkResponse: {
content: {
- 'application/json': components['schemas']['AnonymousUserAuthResponseData']
- }
- }
+ "application/json": components["schemas"]["AnonymousUserAuthResponseData"];
+ };
+ };
/** @description Invalid request data */
GenericBadRequestResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Internal server error */
GenericInternalServerErrorResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Anonymous user id not recognized */
UnauthorizedAnonymousUserResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Logged in */
LoginOkResponse: {
content: {
- 'application/json': components['schemas']['LoginResponseData']
- }
- }
+ "application/json": components["schemas"]["LoginResponseData"];
+ };
+ };
/** @description Account not found by provided address or the address cannot be used to login. */
LoginUnauthorizedResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Cannot create user account with the provided credentials */
CreateAccountBadRequestResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Ok */
GenericOkResponse: {
content: {
- 'application/json': components['schemas']['GenericOkResponseData']
- }
- }
- /** @description Missing token or provided token is invalid / already used. */
- ConfirmEmailBadRequestResponse: {
- content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericOkResponseData"];
+ };
+ };
/** @description Access token (session id) is missing or invalid. */
GenericUnauthorizedResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Request is malformatted or provided e-mail address is not valid. */
RequestTokenBadRequestResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Too many requests for a new token sent within a given timeframe. */
RequestTokenTooManyRequestsResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Too many requests sent within a given timeframe. */
GenericTooManyRequestsResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
- /** @description Provided e-mail address is not associated with any account. */
- RequestEmailConfirmationAccountNotFoundResponse: {
- content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Encryption artifacts found and provided in the response. */
GetArtifactsResponse: {
content: {
- 'application/json': components['schemas']['EncryptionArtifacts']
- }
- }
+ "application/json": components["schemas"]["EncryptionArtifacts"];
+ };
+ };
/** @description Encryption artifacts not found by the provided lookup key. */
GetArtifactsNotFoundResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description No artifacts associated with the current session found. */
GetSessionArtifactsNotFoundResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Account with the provided e-mail address already exists. */
CreateAccountConflictResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Session encryption artifacts for the current session already saved. */
PostSessionArtifactsConflictResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description Provided blockchain (Joystream) account is already assigned to another user account. */
ChangeAccountConflictResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
/** @description On-chain membership not found by the provided memberId. */
CreateAccountNotFoundResponse: {
content: {
- 'application/json': components['schemas']['GenericErrorResponseData']
- }
- }
- }
- parameters: never
+ "application/json": components["schemas"]["GenericErrorResponseData"];
+ };
+ };
+ };
+ parameters: never;
requestBodies: {
RegisterUserInteractionRequestBody?: {
content: {
- 'application/json': components['schemas']['RegisterUserInteractionRequestData']
- }
- }
+ "application/json": components["schemas"]["RegisterUserInteractionRequestData"];
+ };
+ };
AnonymousUserAuthRequestBody?: {
content: {
- 'application/json': components['schemas']['AnonymousUserAuthRequestData']
- }
- }
+ "application/json": components["schemas"]["AnonymousUserAuthRequestData"];
+ };
+ };
LoginRequestBody?: {
content: {
- 'application/json': components['schemas']['LoginRequestData']
- }
- }
+ "application/json": components["schemas"]["LoginRequestData"];
+ };
+ };
CreateAccountRequestBody?: {
content: {
- 'application/json': components['schemas']['CreateAccountRequestData']
- }
- }
- ConfirmEmailRequestBody?: {
- content: {
- 'application/json': components['schemas']['ConfirmEmailRequestData']
- }
- }
+ "application/json": components["schemas"]["CreateAccountRequestData"];
+ };
+ };
RequestTokenRequestBody?: {
content: {
- 'application/json': components['schemas']['RequestTokenRequestData']
- }
- }
+ "application/json": components["schemas"]["RequestTokenRequestData"];
+ };
+ };
PostSessionArtifactsRequestBody?: {
content: {
- 'application/json': components['schemas']['SessionEncryptionArtifacts']
- }
- }
+ "application/json": components["schemas"]["SessionEncryptionArtifacts"];
+ };
+ };
ChangeAccountRequestBody?: {
content: {
- 'application/json': components['schemas']['ChangeAccountRequestData']
- }
- }
- }
- headers: never
- pathItems: never
+ "application/json": components["schemas"]["ChangeAccountRequestData"];
+ };
+ };
+ };
+ headers: never;
+ pathItems: never;
}
-export type $defs = Record
+export type $defs = Record;
-export type external = Record
+export type external = Record;
export interface operations {
+
/** @description Register a user interaction with Atlas part. */
registerUserInteraction: {
- requestBody: components['requestBodies']['RegisterUserInteractionRequestBody']
+ requestBody: components["requestBodies"]["RegisterUserInteractionRequestBody"];
responses: {
- 200: components['responses']['GenericOkResponse']
- 400: components['responses']['GenericBadRequestResponse']
- 401: components['responses']['UnauthorizedAnonymousUserResponse']
- 429: components['responses']['GenericTooManyRequestsResponse']
- default: components['responses']['GenericInternalServerErrorResponse']
- }
- }
+ 200: components["responses"]["GenericOkResponse"];
+ 400: components["responses"]["GenericBadRequestResponse"];
+ 401: components["responses"]["UnauthorizedAnonymousUserResponse"];
+ 429: components["responses"]["GenericTooManyRequestsResponse"];
+ default: components["responses"]["GenericInternalServerErrorResponse"];
+ };
+ };
/** @description Authenticate as an anonymous user, either using an existing user identifier or creating a new one. */
anonymousAuth: {
- requestBody: components['requestBodies']['AnonymousUserAuthRequestBody']
+ requestBody: components["requestBodies"]["AnonymousUserAuthRequestBody"];
responses: {
- 200: components['responses']['AnonymousUserAuthOkResponse']
- 400: components['responses']['GenericBadRequestResponse']
- 401: components['responses']['UnauthorizedAnonymousUserResponse']
- 429: components['responses']['GenericTooManyRequestsResponse']
- default: components['responses']['GenericInternalServerErrorResponse']
- }
- }
+ 200: components["responses"]["AnonymousUserAuthOkResponse"];
+ 400: components["responses"]["GenericBadRequestResponse"];
+ 401: components["responses"]["UnauthorizedAnonymousUserResponse"];
+ 429: components["responses"]["GenericTooManyRequestsResponse"];
+ default: components["responses"]["GenericInternalServerErrorResponse"];
+ };
+ };
/** @description Login to user's account by providing a message signed by the associated blockchain account. */
login: {
- requestBody: components['requestBodies']['LoginRequestBody']
+ requestBody: components["requestBodies"]["LoginRequestBody"];
responses: {
- 200: components['responses']['LoginOkResponse']
- 400: components['responses']['GenericBadRequestResponse']
- 401: components['responses']['LoginUnauthorizedResponse']
- 429: components['responses']['GenericTooManyRequestsResponse']
- default: components['responses']['GenericInternalServerErrorResponse']
- }
- }
+ 200: components["responses"]["LoginOkResponse"];
+ 400: components["responses"]["GenericBadRequestResponse"];
+ 401: components["responses"]["LoginUnauthorizedResponse"];
+ 429: components["responses"]["GenericTooManyRequestsResponse"];
+ default: components["responses"]["GenericInternalServerErrorResponse"];
+ };
+ };
/** @description Get wallet seed encryption artifacts. */
getArtifacts: {
parameters: {
query: {
/** @description The lookup key derived from user's credentials. */
- id: string
+ id: string;
/** @description The user's email address. */
- email: string
- }
- }
+ email: string;
+ };
+ };
responses: {
- 200: components['responses']['GetArtifactsResponse']
- 400: components['responses']['GenericBadRequestResponse']
- 404: components['responses']['GetArtifactsNotFoundResponse']
- 429: components['responses']['GenericTooManyRequestsResponse']
- default: components['responses']['GenericInternalServerErrorResponse']
- }
- }
+ 200: components["responses"]["GetArtifactsResponse"];
+ 400: components["responses"]["GenericBadRequestResponse"];
+ 404: components["responses"]["GetArtifactsNotFoundResponse"];
+ 429: components["responses"]["GenericTooManyRequestsResponse"];
+ default: components["responses"]["GenericInternalServerErrorResponse"];
+ };
+ };
/** @description Get wallet seed encryption artifacts for the current session. */
getSessionArtifacts: {
responses: {
- 200: components['responses']['GetArtifactsResponse']
- 401: components['responses']['GenericUnauthorizedResponse']
- 404: components['responses']['GetSessionArtifactsNotFoundResponse']
- 429: components['responses']['GenericTooManyRequestsResponse']
- default: components['responses']['GenericInternalServerErrorResponse']
- }
- }
+ 200: components["responses"]["GetArtifactsResponse"];
+ 401: components["responses"]["GenericUnauthorizedResponse"];
+ 404: components["responses"]["GetSessionArtifactsNotFoundResponse"];
+ 429: components["responses"]["GenericTooManyRequestsResponse"];
+ default: components["responses"]["GenericInternalServerErrorResponse"];
+ };
+ };
/** @description Save wallet seed encryption artifacts for the current session on the server. */
postSessionArtifacts: {
- requestBody: components['requestBodies']['PostSessionArtifactsRequestBody']
+ requestBody: components["requestBodies"]["PostSessionArtifactsRequestBody"];
responses: {
- 200: components['responses']['GenericOkResponse']
- 400: components['responses']['GenericBadRequestResponse']
- 401: components['responses']['GenericUnauthorizedResponse']
- 409: components['responses']['PostSessionArtifactsConflictResponse']
- 429: components['responses']['GenericTooManyRequestsResponse']
- default: components['responses']['GenericInternalServerErrorResponse']
- }
- }
+ 200: components["responses"]["GenericOkResponse"];
+ 400: components["responses"]["GenericBadRequestResponse"];
+ 401: components["responses"]["GenericUnauthorizedResponse"];
+ 409: components["responses"]["PostSessionArtifactsConflictResponse"];
+ 429: components["responses"]["GenericTooManyRequestsResponse"];
+ default: components["responses"]["GenericInternalServerErrorResponse"];
+ };
+ };
/** @description Create a new Gateway account. Requires anonymousAuth to be performed first. */
createAccount: {
- requestBody: components['requestBodies']['CreateAccountRequestBody']
- responses: {
- 200: components['responses']['GenericOkResponse']
- 400: components['responses']['CreateAccountBadRequestResponse']
- 401: components['responses']['GenericUnauthorizedResponse']
- 404: components['responses']['CreateAccountNotFoundResponse']
- 409: components['responses']['CreateAccountConflictResponse']
- 429: components['responses']['GenericTooManyRequestsResponse']
- default: components['responses']['GenericInternalServerErrorResponse']
- }
- }
- /** @description Confirm account's e-mail address provided during registration. */
- confirmEmail: {
- requestBody: components['requestBodies']['ConfirmEmailRequestBody']
+ requestBody: components["requestBodies"]["CreateAccountRequestBody"];
responses: {
- 200: components['responses']['GenericOkResponse']
- 400: components['responses']['ConfirmEmailBadRequestResponse']
- 429: components['responses']['GenericTooManyRequestsResponse']
- default: components['responses']['GenericInternalServerErrorResponse']
- }
- }
- /** @description Request a token to be sent to account's e-mail address, which will allow confirming the ownership of the e-mail by the user. */
+ 200: components["responses"]["GenericOkResponse"];
+ 400: components["responses"]["CreateAccountBadRequestResponse"];
+ 401: components["responses"]["GenericUnauthorizedResponse"];
+ 404: components["responses"]["CreateAccountNotFoundResponse"];
+ 409: components["responses"]["CreateAccountConflictResponse"];
+ 429: components["responses"]["GenericTooManyRequestsResponse"];
+ default: components["responses"]["GenericInternalServerErrorResponse"];
+ };
+ };
+ /** @description Request a token to be sent to e-mail address (as the first step of signup process), which will allow confirming the ownership of the e-mail by the user. */
requestEmailConfirmationToken: {
- requestBody: components['requestBodies']['RequestTokenRequestBody']
+ requestBody: components["requestBodies"]["RequestTokenRequestBody"];
responses: {
- 200: components['responses']['GenericOkResponse']
- 400: components['responses']['RequestTokenBadRequestResponse']
- 404: components['responses']['RequestEmailConfirmationAccountNotFoundResponse']
- 429: components['responses']['RequestTokenTooManyRequestsResponse']
- default: components['responses']['GenericInternalServerErrorResponse']
- }
- }
+ 200: components["responses"]["GenericOkResponse"];
+ 400: components["responses"]["RequestTokenBadRequestResponse"];
+ 429: components["responses"]["RequestTokenTooManyRequestsResponse"];
+ default: components["responses"]["GenericInternalServerErrorResponse"];
+ };
+ };
/** @description Change the blockchain (Joystream) account associated with the Gateway account. Delete the old account's encryption artifacts and optionally set new ones. */
changeAccount: {
- requestBody: components['requestBodies']['ChangeAccountRequestBody']
+ requestBody: components["requestBodies"]["ChangeAccountRequestBody"];
responses: {
- 200: components['responses']['GenericOkResponse']
- 400: components['responses']['GenericBadRequestResponse']
- 401: components['responses']['GenericUnauthorizedResponse']
- 409: components['responses']['ChangeAccountConflictResponse']
- 429: components['responses']['GenericTooManyRequestsResponse']
- default: components['responses']['GenericInternalServerErrorResponse']
- }
- }
+ 200: components["responses"]["GenericOkResponse"];
+ 400: components["responses"]["GenericBadRequestResponse"];
+ 401: components["responses"]["GenericUnauthorizedResponse"];
+ 409: components["responses"]["ChangeAccountConflictResponse"];
+ 429: components["responses"]["GenericTooManyRequestsResponse"];
+ default: components["responses"]["GenericInternalServerErrorResponse"];
+ };
+ };
/** @description Terminate the current session. */
logout: {
responses: {
- 200: components['responses']['GenericOkResponse']
- 401: components['responses']['GenericUnauthorizedResponse']
- 429: components['responses']['GenericTooManyRequestsResponse']
- default: components['responses']['GenericInternalServerErrorResponse']
- }
- }
+ 200: components["responses"]["GenericOkResponse"];
+ 401: components["responses"]["GenericUnauthorizedResponse"];
+ 429: components["responses"]["GenericTooManyRequestsResponse"];
+ default: components["responses"]["GenericInternalServerErrorResponse"];
+ };
+ };
}
diff --git a/src/auth-server/handlers/changeAccount.ts b/src/auth-server/handlers/changeAccount.ts
index 22dccb26d..a13f9f66d 100644
--- a/src/auth-server/handlers/changeAccount.ts
+++ b/src/auth-server/handlers/changeAccount.ts
@@ -1,9 +1,8 @@
import express from 'express'
-import { components } from '../generated/api-types'
-import { UnauthorizedError, BadRequestError, ConflictError } from '../errors'
-import { AuthContext } from '../../utils/auth'
+import { Account, BlockchainAccount, EncryptionArtifacts, Session } from '../../model'
import { globalEm } from '../../utils/globalEm'
-import { Account, EncryptionArtifacts } from '../../model'
+import { BadRequestError, ConflictError, UnauthorizedError } from '../errors'
+import { components } from '../generated/api-types'
import { verifyActionExecutionRequest } from '../utils'
type ReqParams = Record
@@ -11,7 +10,7 @@ type ResBody =
| components['schemas']['GenericOkResponseData']
| components['schemas']['GenericErrorResponseData']
type ReqBody = components['schemas']['ChangeAccountRequestData']
-type ResLocals = { authContext: AuthContext }
+type ResLocals = { authContext: Session }
export const changeAccount: (
req: express.Request,
@@ -19,14 +18,10 @@ export const changeAccount: (
next: express.NextFunction
) => Promise = async (req, res, next) => {
try {
- const {
- body: { payload },
- } = req
- const {
- locals: { authContext },
- } = res
+ const { payload } = req.body
+ const { authContext } = res.locals
- if (!authContext?.account) {
+ if (!authContext.account) {
throw new UnauthorizedError()
}
@@ -41,7 +36,7 @@ export const changeAccount: (
await em.transaction(async (em) => {
const existingGatewayAccount = await em.findOne(Account, {
- where: { joystreamAccount: payload.joystreamAccountId },
+ where: { joystreamAccountId: payload.joystreamAccountId },
})
if (existingGatewayAccount && existingGatewayAccount.id !== account.id) {
@@ -50,15 +45,21 @@ export const changeAccount: (
)
}
- // Update the assigned blockchain account
- await em.update(Account, { id: account.id }, { joystreamAccount: payload.joystreamAccountId })
+ // Create the given blockchain account if it doesn't exist
+ await em.upsert(BlockchainAccount, { id: payload.joystreamAccountId }, ['id'])
+
+ await em.update(
+ Account,
+ { id: account.id },
+ { joystreamAccountId: payload.joystreamAccountId }
+ )
// Remove the old encryption artifacts
await em.delete(EncryptionArtifacts, { accountId: account.id })
// Optionally save new encryption artifacts
if (payload.newArtifacts) {
- // We don't check if artifacts already exist by this id, becasue that opens up
+ // We don't check if artifacts already exist by this id, because that opens up
// a brute-force attack vector. Instead, in this case the existing artifacts will
// be overwritten.
await em.save(EncryptionArtifacts, {
diff --git a/src/auth-server/handlers/confirmEmail.ts b/src/auth-server/handlers/confirmEmail.ts
deleted file mode 100644
index 6d25dfeb8..000000000
--- a/src/auth-server/handlers/confirmEmail.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import express from 'express'
-import { BadRequestError } from '../errors'
-import { components } from '../generated/api-types'
-import { globalEm } from '../../utils/globalEm'
-import { Token } from '../../model'
-import { MoreThan } from 'typeorm'
-
-type ReqParams = Record
-type ResBody =
- | components['schemas']['GenericOkResponseData']
- | components['schemas']['GenericErrorResponseData']
-type ReqBody = components['schemas']['ConfirmEmailRequestData']
-
-export const confirmEmail: (
- req: express.Request,
- res: express.Response,
- next: express.NextFunction
-) => Promise = async (req, res, next) => {
- try {
- const { token: tokenId } = req.body
- const em = await globalEm
-
- await em.transaction(async (em) => {
- const token = await em.getRepository(Token).findOne({
- where: { id: tokenId, expiry: MoreThan(new Date()) },
- relations: { issuedFor: true },
- })
-
- if (!token) {
- throw new BadRequestError('Token not found. Possibly expired or already used.')
- }
-
- if (token.issuedFor.isEmailConfirmed) {
- throw new BadRequestError('Email already confirmed')
- }
-
- const account = token.issuedFor
- account.isEmailConfirmed = true
- token.expiry = new Date()
- await em.save([account, token])
- })
-
- res.status(200).json({ success: true })
- } catch (e) {
- next(e)
- }
-}
diff --git a/src/auth-server/handlers/createAccount.ts b/src/auth-server/handlers/createAccount.ts
index 096927fa3..18b035158 100644
--- a/src/auth-server/handlers/createAccount.ts
+++ b/src/auth-server/handlers/createAccount.ts
@@ -1,6 +1,13 @@
import express from 'express'
-import { Account, EncryptionArtifacts, Membership, NextEntityId } from '../../model'
-import { AuthContext } from '../../utils/auth'
+import { MoreThan } from 'typeorm'
+import {
+ Account,
+ BlockchainAccount,
+ EmailConfirmationToken,
+ EncryptionArtifacts,
+ NextEntityId,
+ Session,
+} from '../../model'
import { globalEm } from '../../utils/globalEm'
import { idStringFromNumber } from '../../utils/misc'
import { defaultNotificationPreferences } from '../../utils/notification/helpers'
@@ -13,7 +20,7 @@ type ResBody =
| components['schemas']['GenericOkResponseData']
| components['schemas']['GenericErrorResponseData']
type ReqBody = components['schemas']['CreateAccountRequestData']
-type ResLocals = { authContext: AuthContext }
+type ResLocals = { authContext: Session }
export const createAccount: (
req: express.Request,
@@ -22,15 +29,23 @@ export const createAccount: (
) => Promise = async (req, res, next) => {
try {
const {
- payload: { email, memberId, joystreamAccountId },
+ payload: { email, emailConfirmationToken, joystreamAccountId },
} = req.body
const { authContext } = res.locals
const em = await globalEm
- if (authContext?.account) {
+ if (authContext.account) {
throw new BadRequestError('Already logged in to an account.')
}
+ const token = await em.getRepository(EmailConfirmationToken).findOne({
+ where: { id: emailConfirmationToken, email, expiry: MoreThan(new Date()) },
+ })
+
+ if (!token) {
+ throw new NotFoundError('Token not found. Possibly expired or already used.')
+ }
+
await verifyActionExecutionRequest(em, req.body)
await em.transaction(async (em) => {
@@ -45,55 +60,43 @@ export const createAccount: (
)?.nextId.toString() || '1'
)
- const existingByEmail = await em.getRepository(Account).findOneBy({ email })
- if (existingByEmail) {
- throw new ConflictError('Account with the provided e-mail address already exists.')
- }
+ // TODO: Don't reveal whether an account with the given email exists, to prevent email enumeration attacks
+ // ! Not needed, as the token is already checked for existence, and is not expired
+ // const existingByEmail = await em.getRepository(Account).findOneBy({ email })
+ // if (existingByEmail) {
+ // throw new ConflictError('Account with the provided e-mail address already exists.')
+ // }
- const existingByMemberId = await em
- .getRepository(Account)
- .findOneBy({ membershipId: memberId })
- if (existingByMemberId) {
- throw new ConflictError('Account with the provided member id already exists.')
- }
+ // Create the given blockchain account if it doesn't exist
+ await em.upsert(BlockchainAccount, { id: joystreamAccountId }, ['id'])
const existingByJoystreamAccountId = await em
.getRepository(Account)
- .findOneBy({ joystreamAccount: joystreamAccountId })
+ .findOneBy({ joystreamAccountId })
if (existingByJoystreamAccountId) {
throw new ConflictError(
'Account with the provided joystream account address already exists.'
)
}
- const membership = await em.getRepository(Membership).findOneBy({ id: memberId })
- if (!membership) {
- throw new NotFoundError(`Membership not found by id: ${memberId}`)
- }
-
- if (membership.controllerAccount !== joystreamAccountId) {
- throw new BadRequestError(
- `Provided joystream account address doesn't match the controller account of the provided membership.`
- )
- }
-
- const notificationPreferences = defaultNotificationPreferences()
+ // Create the account
const account = new Account({
id: idStringFromNumber(nextAccountId),
email,
- isEmailConfirmed: false,
registeredAt: new Date(),
isBlocked: false,
- userId: authContext?.user.id,
- joystreamAccount: joystreamAccountId,
- membershipId: memberId.toString(),
- notificationPreferences,
- referrerChannelId: null,
+ userId: authContext.user.id,
+ joystreamAccountId: joystreamAccountId,
+ notificationPreferences: defaultNotificationPreferences(),
})
+ // Mark the token as used
+ token.expiry = new Date()
+
await em.save([
account,
new NextEntityId({ entityName: 'Account', nextId: nextAccountId + 1 }),
+ token,
])
if (req.body.payload.encryptionArtifacts) {
diff --git a/src/auth-server/handlers/getSessionArtifacts.ts b/src/auth-server/handlers/getSessionArtifacts.ts
index 2ba3fe832..729ce92c8 100644
--- a/src/auth-server/handlers/getSessionArtifacts.ts
+++ b/src/auth-server/handlers/getSessionArtifacts.ts
@@ -1,15 +1,15 @@
import express from 'express'
-import { NotFoundError, UnauthorizedError } from '../errors'
+import { Session, SessionEncryptionArtifacts } from '../../model'
import { globalEm } from '../../utils/globalEm'
-import { SessionEncryptionArtifacts } from '../../model'
+import { NotFoundError, UnauthorizedError } from '../errors'
import { components } from '../generated/api-types'
-import { AuthContext } from '../../utils/auth'
+// TODO: ensure that encryption artifacts for expired sessions are removed
type ReqParams = Record
type ResBody =
| components['schemas']['SessionEncryptionArtifacts']
| components['schemas']['GenericErrorResponseData']
-type ResLocals = { authContext: AuthContext }
+type ResLocals = { authContext: Session }
export const getSessionArtifacts: (
req: express.Request,
@@ -19,7 +19,7 @@ export const getSessionArtifacts: (
try {
const em = await globalEm
const { authContext: session } = res.locals
- if (!session?.account) {
+ if (!session.account) {
throw new UnauthorizedError('Cannot get session artifacts for anonymous session')
}
const artifacts = await em
diff --git a/src/auth-server/handlers/login.ts b/src/auth-server/handlers/login.ts
index 723f9c9fe..902448652 100644
--- a/src/auth-server/handlers/login.ts
+++ b/src/auth-server/handlers/login.ts
@@ -1,9 +1,9 @@
import express from 'express'
import { Account } from '../../model'
+import { getOrCreateSession, setSessionCookie } from '../../utils/auth'
import { globalEm } from '../../utils/globalEm'
import { UnauthorizedError } from '../errors'
import { components } from '../generated/api-types'
-import { getOrCreateSession, setSessionCookie } from '../../utils/auth'
import { verifyActionExecutionRequest } from '../utils'
type ReqParams = Record
@@ -26,9 +26,7 @@ export const login: (
await verifyActionExecutionRequest(em, req.body)
const [sessionData, account] = await em.transaction(async (em) => {
- const account = await em
- .getRepository(Account)
- .findOneBy({ joystreamAccount: joystreamAccountId })
+ const account = await em.getRepository(Account).findOneBy({ joystreamAccountId })
if (!account) {
throw new UnauthorizedError('Invalid credentials')
}
diff --git a/src/auth-server/handlers/logout.ts b/src/auth-server/handlers/logout.ts
index b7c42f41c..f19cc7ac5 100644
--- a/src/auth-server/handlers/logout.ts
+++ b/src/auth-server/handlers/logout.ts
@@ -1,14 +1,14 @@
import express from 'express'
-import { AuthContext } from '../../utils/auth'
+import { Session } from '../../model'
import { globalEm } from '../../utils/globalEm'
-import { components } from '../generated/api-types'
import { BadRequestError } from '../errors'
+import { components } from '../generated/api-types'
type ReqParams = Record
type ResBody =
| components['schemas']['GenericOkResponseData']
| components['schemas']['GenericErrorResponseData']
-type ResLocals = { authContext: AuthContext }
+type ResLocals = { authContext: Session }
export const logout: (
req: express.Request,
diff --git a/src/auth-server/handlers/postSessionArtifacts.ts b/src/auth-server/handlers/postSessionArtifacts.ts
index dd9bd9d4f..b824249e7 100644
--- a/src/auth-server/handlers/postSessionArtifacts.ts
+++ b/src/auth-server/handlers/postSessionArtifacts.ts
@@ -1,17 +1,16 @@
import express from 'express'
-import { AuthContext } from '../../utils/auth'
-import { globalEm } from '../../utils/globalEm'
-import { components } from '../generated/api-types'
-import { SessionEncryptionArtifacts } from '../../model'
+import { Session, SessionEncryptionArtifacts } from '../../model'
import { uniqueId } from '../../utils/crypto'
+import { globalEm } from '../../utils/globalEm'
import { ConflictError, UnauthorizedError } from '../errors'
+import { components } from '../generated/api-types'
type ReqParams = Record
type ReqBody = components['schemas']['SessionEncryptionArtifacts']
type ResBody =
| components['schemas']['GenericOkResponseData']
| components['schemas']['GenericErrorResponseData']
-type ResLocals = { authContext: AuthContext }
+type ResLocals = { authContext: Session }
export const postSessionArtifacts: (
req: express.Request,
diff --git a/src/auth-server/handlers/registerUserInteraction.ts b/src/auth-server/handlers/registerUserInteraction.ts
index 425184c07..5ee7e4062 100644
--- a/src/auth-server/handlers/registerUserInteraction.ts
+++ b/src/auth-server/handlers/registerUserInteraction.ts
@@ -1,11 +1,9 @@
import express from 'express'
-import { AuthContext } from '../../utils/auth'
+import { InMemoryRateLimiter } from 'rolling-rate-limiter'
+import { Session, UserInteractionCount } from '../../model'
import { globalEm } from '../../utils/globalEm'
-import { components } from '../generated/api-types'
import { TooManyRequestsError, UnauthorizedError } from '../errors'
-import { UserInteractionCount } from '../../model'
-
-import { InMemoryRateLimiter } from 'rolling-rate-limiter'
+import { components } from '../generated/api-types'
const interactionLimiter = new InMemoryRateLimiter({
interval: 1000 * 60 * 5, // 5 minutes
@@ -16,7 +14,7 @@ type ReqParams = Record
type ResBody =
| components['schemas']['GenericOkResponseData']
| components['schemas']['GenericErrorResponseData']
-type ResLocals = { authContext: AuthContext }
+type ResLocals = { authContext: Session }
type ReqBody = components['schemas']['RegisterUserInteractionRequestData']
export const registerUserInteraction: (
diff --git a/src/auth-server/handlers/requestEmailConfirmationToken.ts b/src/auth-server/handlers/requestEmailConfirmationToken.ts
index ee00e628e..d9e741ed8 100644
--- a/src/auth-server/handlers/requestEmailConfirmationToken.ts
+++ b/src/auth-server/handlers/requestEmailConfirmationToken.ts
@@ -1,25 +1,31 @@
import express from 'express'
-import { NotFoundError, TooManyRequestsError, BadRequestError } from '../errors'
-import { components } from '../generated/api-types'
-import { globalEm } from '../../utils/globalEm'
-import { Account, Token, TokenType } from '../../model'
import { MoreThan } from 'typeorm'
+import { Account, EmailConfirmationToken, Session } from '../../model'
import { ConfigVariable, config } from '../../utils/config'
-import { sendWelcomeEmail } from '../utils'
+import { globalEm } from '../../utils/globalEm'
+import { BadRequestError, TooManyRequestsError } from '../errors'
+import { components } from '../generated/api-types'
+import { sendLoginOrChangePasswordEmail, sendWelcomeEmail } from '../utils'
type ReqParams = Record
type ResBody =
| components['schemas']['GenericOkResponseData']
| components['schemas']['GenericErrorResponseData']
type ReqBody = components['schemas']['RequestTokenRequestData']
+type ResLocals = { authContext: Session }
export const requestEmailConfirmationToken: (
req: express.Request,
- res: express.Response,
+ res: express.Response,
next: express.NextFunction
) => Promise = async (req, res, next) => {
try {
- const { email } = req.body
+ const { email, signupType } = req.body
+ const { authContext } = res.locals
+
+ if (authContext.account) {
+ throw new BadRequestError('Already logged in to an account.')
+ }
const em = await globalEm
@@ -38,18 +44,9 @@ export const requestEmailConfirmationToken: (
lock: { mode: 'pessimistic_write' },
})
- if (!account) {
- throw new NotFoundError('Account not found')
- }
-
- if (account.isEmailConfirmed) {
- throw new BadRequestError('Email already confirmed')
- }
-
- const tokensInTimeframeCount = await em.getRepository(Token).count({
+ const tokensInTimeframeCount = await em.getRepository(EmailConfirmationToken).count({
where: {
- issuedForId: account.id,
- type: TokenType.EMAIL_CONFIRMATION,
+ email,
issuedAt: MoreThan(
new Date(Date.now() - emailConfirmationTokenExpiryTimeHours * 3600 * 1000)
),
@@ -60,17 +57,20 @@ export const requestEmailConfirmationToken: (
throw new TooManyRequestsError()
}
- // Deactivate all currently active email confirmation tokens for this account
- await em.getRepository(Token).update(
- {
- issuedForId: account.id,
- type: TokenType.EMAIL_CONFIRMATION,
- expiry: MoreThan(new Date()),
- },
- { expiry: new Date() }
- )
+ // Deactivate all currently active email confirmation tokens for this email
+ const currentTimestamp = new Date()
+ await em
+ .getRepository(EmailConfirmationToken)
+ .update({ email, expiry: MoreThan(currentTimestamp) }, { expiry: currentTimestamp })
- await sendWelcomeEmail(account, em)
+ // Send email with verification token if Gateway account does not exist.
+ // Otherwise, send email to asking user to login/change password. but
+ // the API response should be the same to avoid email enumeration attack.
+ if (account) {
+ await sendLoginOrChangePasswordEmail(email, em)
+ } else {
+ await sendWelcomeEmail(email, signupType, em)
+ }
})
res.status(200).json({ success: true })
diff --git a/src/auth-server/index.ts b/src/auth-server/index.ts
index 991a88ceb..5b25fdcab 100644
--- a/src/auth-server/index.ts
+++ b/src/auth-server/index.ts
@@ -1,16 +1,16 @@
-import express from 'express'
+import { createLogger } from '@subsquid/logger'
+import cookieParser from 'cookie-parser'
import cors from 'cors'
+import express from 'express'
import * as OpenApiValidator from 'express-openapi-validator'
import { HttpError } from 'express-openapi-validator/dist/framework/types'
+import fs from 'fs'
+import YAML from 'js-yaml'
import path from 'path'
-import { AuthApiError } from './errors'
-import { createLogger } from '@subsquid/logger'
+import swaggerUi, { JsonObject } from 'swagger-ui-express'
import { authenticate, getCorsOrigin } from '../utils/auth'
-import cookieParser from 'cookie-parser'
+import { AuthApiError } from './errors'
import { applyRateLimits, globalRateLimit, rateLimitsPerRoute } from './rateLimits'
-import swaggerUi, { JsonObject } from 'swagger-ui-express'
-import YAML from 'js-yaml'
-import fs from 'fs'
export const logger = createLogger('auth-api')
@@ -22,7 +22,7 @@ function authHandler(type: 'header' | 'cookie') {
if (req.res) {
req.res.locals.authContext = authContext
}
- return true
+ return !!authContext
}
}
@@ -42,7 +42,6 @@ if (process.env.OPENAPI_PLAYGROUND === 'true' || process.env.OPENAPI_PLAYGROUND
fs.readFileSync(path.join(__dirname, 'openapi.yml')).toString()
) as JsonObject
logger.info('Running playground at /playground')
- console.log('Spec', spec)
app.use('/playground', swaggerUi.serve, swaggerUi.setup(spec))
}
app.use(
diff --git a/src/auth-server/openapi.yml b/src/auth-server/openapi.yml
index 498b5b50d..da6694550 100644
--- a/src/auth-server/openapi.yml
+++ b/src/auth-server/openapi.yml
@@ -160,28 +160,15 @@ paths:
$ref: '#/components/responses/GenericTooManyRequestsResponse'
default:
$ref: '#/components/responses/GenericInternalServerErrorResponse'
- /confirm-email:
- post:
- operationId: confirmEmail
- x-eov-operation-handler: confirmEmail
- description: Confirm account's e-mail address provided during registration.
- requestBody:
- $ref: '#/components/requestBodies/ConfirmEmailRequestBody'
- responses:
- '200':
- $ref: '#/components/responses/GenericOkResponse'
- '400':
- $ref: '#/components/responses/ConfirmEmailBadRequestResponse'
- '429':
- $ref: '#/components/responses/GenericTooManyRequestsResponse'
- default:
- $ref: '#/components/responses/GenericInternalServerErrorResponse'
/request-email-confirmation-token:
post:
operationId: requestEmailConfirmationToken
x-eov-operation-handler: requestEmailConfirmationToken
- description: Request a token to be sent to account's e-mail address,
+ description:
+ Request a token to be sent to e-mail address (as the first step of signup process),
which will allow confirming the ownership of the e-mail by the user.
+ security:
+ - cookieAuth: []
requestBody:
$ref: '#/components/requestBodies/RequestTokenRequestBody'
responses:
@@ -189,8 +176,6 @@ paths:
$ref: '#/components/responses/GenericOkResponse'
'400':
$ref: '#/components/responses/RequestTokenBadRequestResponse'
- '404':
- $ref: '#/components/responses/RequestEmailConfirmationAccountNotFoundResponse'
'429':
$ref: '#/components/responses/RequestTokenTooManyRequestsResponse'
default:
@@ -265,11 +250,6 @@ components:
application/json:
schema:
$ref: '#/components/schemas/CreateAccountRequestData'
- ConfirmEmailRequestBody:
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ConfirmEmailRequestData'
RequestTokenRequestBody:
content:
application/json:
@@ -334,12 +314,6 @@ components:
application/json:
schema:
$ref: '#/components/schemas/GenericOkResponseData'
- ConfirmEmailBadRequestResponse:
- description: Missing token or provided token is invalid / already used.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/GenericErrorResponseData'
GenericUnauthorizedResponse:
description: Access token (session id) is missing or invalid.
content:
@@ -364,12 +338,6 @@ components:
application/json:
schema:
$ref: '#/components/schemas/GenericErrorResponseData'
- RequestEmailConfirmationAccountNotFoundResponse:
- description: Provided e-mail address is not associated with any account.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/GenericErrorResponseData'
GetArtifactsResponse:
description: Encryption artifacts found and provided in the response.
content:
@@ -496,18 +464,17 @@ components:
- type: object
required:
- email
- - memberId
properties:
action:
type: string
enum:
- createAccount
- memberId:
- type: string
email:
type: string
pattern: ^\S+@\S+\.\S+$
maxLength: 255
+ emailConfirmationToken:
+ type: string
encryptionArtifacts:
$ref: '#/components/schemas/EncryptionArtifacts'
ChangeAccountRequestData:
@@ -530,22 +497,22 @@ components:
type: string
newArtifacts:
$ref: '#/components/schemas/EncryptionArtifacts'
- ConfirmEmailRequestData:
- type: object
- required:
- - token
- properties:
- token:
- type: string
- description: Confirmation token recieved by the user via an e-mail.
RequestTokenRequestData:
type: object
required:
- email
+ - signupType
properties:
email:
type: string
description: User's e-mail address.
+ signupType:
+ type: string
+ enum:
+ - internal
+ - external
+ - ypp
+ description: Signup type.
GenericErrorResponseData:
type: object
properties:
diff --git a/src/auth-server/rateLimits.ts b/src/auth-server/rateLimits.ts
index 28191c150..68b7efa9a 100644
--- a/src/auth-server/rateLimits.ts
+++ b/src/auth-server/rateLimits.ts
@@ -1,6 +1,6 @@
+import { Express } from 'express'
import rateLimit, { Options as RateLimitOptions, RateLimitRequestHandler } from 'express-rate-limit'
import { paths } from './generated/api-types'
-import { Express } from 'express'
const defaultRateLimitOptions: Partial = {
standardHeaders: true,
@@ -75,12 +75,6 @@ export const rateLimitsPerRoute: RateLimitsPerRoute = {
limit: 10,
},
},
- '/confirm-email': {
- post: {
- windowMinutes: 5,
- limit: 10,
- },
- },
}
const limiters: RateLimitRequestHandler[] = []
diff --git a/src/auth-server/tests/changeAccount.ts b/src/auth-server/tests/changeAccount.ts
index 3be983f9a..0124ec261 100644
--- a/src/auth-server/tests/changeAccount.ts
+++ b/src/auth-server/tests/changeAccount.ts
@@ -1,12 +1,15 @@
-import './config'
-import request from 'supertest'
-import { app } from '../index'
-import { globalEm } from '../../utils/globalEm'
-import assert from 'assert'
-import { ConfigVariable, config } from '../../utils/config'
-import { u8aToHex } from '@polkadot/util'
import { KeyringPair } from '@polkadot/keyring/types'
+import { u8aToHex } from '@polkadot/util'
+import { cryptoWaitReady } from '@polkadot/util-crypto'
+import assert from 'assert'
+import request from 'supertest'
import { EntityManager } from 'typeorm'
+import { Account, EncryptionArtifacts as EncryptionArtifactsEntity } from '../../model'
+import { SESSION_COOKIE_NAME } from '../../utils/auth'
+import { ConfigVariable, config } from '../../utils/config'
+import { globalEm } from '../../utils/globalEm'
+import { components } from '../generated/api-types'
+import { app } from '../index'
import {
EncryptionArtifacts,
LoggedInAccountInfo,
@@ -16,10 +19,7 @@ import {
keyring,
prepareEncryptionArtifacts,
} from './common'
-import { cryptoWaitReady } from '@polkadot/util-crypto'
-import { components } from '../generated/api-types'
-import { SESSION_COOKIE_NAME } from '../../utils/auth'
-import { Account, EncryptionArtifacts as EncryptionArtifactsEntity } from '../../model'
+import './config'
type ChangeAccountArgs = {
accountId: string
@@ -94,7 +94,7 @@ describe('changeAccount', () => {
accountId,
})
assert(
- account?.joystreamAccount === joystreamAccountId,
+ account?.joystreamAccountId === joystreamAccountId,
'Blockchain account unexpectedly changed'
)
assert(encryptionArtifacts, 'Encryption artifacts unexpectedly deleted')
@@ -226,7 +226,7 @@ describe('changeAccount', () => {
.getRepository(EncryptionArtifactsEntity)
.findOneBy({ accountId })
assert(
- account?.joystreamAccount === joystreamAccountId,
+ account?.joystreamAccountId === joystreamAccountId,
'Blockchain account unexpectedly changed'
)
assert(encryptionArtifacts, 'New encryption artifacts not saved')
@@ -253,7 +253,7 @@ describe('changeAccount', () => {
const encryptionArtifacts = await em
.getRepository(EncryptionArtifactsEntity)
.findOneBy({ accountId })
- assert(account?.joystreamAccount === alice.address, 'Blockchain account not changed')
+ assert(account?.joystreamAccountId === alice.address, 'Blockchain account not changed')
assert(encryptionArtifacts, 'Encryption artifacts not saved')
assert(
(await decryptSeed(email, password, encryptionArtifacts)) === newSeed,
@@ -275,7 +275,7 @@ describe('changeAccount', () => {
const encryptionArtifacts = await em
.getRepository(EncryptionArtifactsEntity)
.findOneBy({ accountId })
- assert(account?.joystreamAccount === bob.address, 'Blockchain account not changed')
+ assert(account?.joystreamAccountId === bob.address, 'Blockchain account not changed')
assert(!encryptionArtifacts, 'Encryption artifacts not deleted')
})
diff --git a/src/auth-server/tests/common.ts b/src/auth-server/tests/common.ts
index a5cbe236a..1e84487a2 100644
--- a/src/auth-server/tests/common.ts
+++ b/src/auth-server/tests/common.ts
@@ -1,19 +1,20 @@
-import './config'
-import request from 'supertest'
-import { app } from '../index'
-import { globalEm } from '../../utils/globalEm'
-import { Account, Membership } from '../../model'
-import assert from 'assert'
-import { components } from '../generated/api-types'
+import { JOYSTREAM_ADDRESS_PREFIX } from '@joystream/types'
import { Keyring } from '@polkadot/keyring'
import { KeyringPair } from '@polkadot/keyring/types'
-import { ConfigVariable, config } from '../../utils/config'
import { u8aToHex } from '@polkadot/util'
-import { JOYSTREAM_ADDRESS_PREFIX } from '@joystream/types'
-import { uniqueId } from '../../utils/crypto'
+import assert from 'assert'
import { ScryptOptions, createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto'
+import request from 'supertest'
+import { Account, Membership } from '../../model'
import { SESSION_COOKIE_NAME } from '../../utils/auth'
+import { ConfigVariable, config } from '../../utils/config'
+import { uniqueId } from '../../utils/crypto'
+import { globalEm } from '../../utils/globalEm'
+import { components } from '../generated/api-types'
+import { app } from '../index'
import { SimpleRateLimit, resetAllLimits } from '../rateLimits'
+import './config'
+import { findActiveToken } from './requestEmailConfirmationToken'
export const keyring = new Keyring({ type: 'sr25519', ss58Format: JOYSTREAM_ADDRESS_PREFIX })
@@ -125,13 +126,20 @@ async function insertFakeMember(controllerAccount: string) {
return em.getRepository(Membership).save({
createdAt: new Date(),
id: uniqueId(),
- controllerAccount,
+ controllerAccountId: controllerAccount,
handle: uniqueId(),
handleRaw: '0x' + Buffer.from(handle).toString('hex'),
totalChannelsCreated: 0,
})
}
+async function getAccountByEmail(email: string) {
+ const em = await globalEm
+ return em
+ .getRepository(Account)
+ .findOne({ where: { email }, relations: { joystreamAccount: { memberships: true } } })
+}
+
export async function createAccount(
email = `test.${uniqueId()}@example.com`,
password = DEFAULT_PASSWORD,
@@ -141,16 +149,21 @@ export async function createAccount(
const keypair = keyring.addFromUri(`//${seed}`)
const em = await globalEm
- const membership = await insertFakeMember(keypair.address)
+ // const membership = await insertFakeMember(keypair.address)
const anonSessionId = await anonymousAuth()
+ await requestEmailConfirmationToken(email, anonSessionId, 200)
+
+ const token = await findActiveToken(em, email)
+ assert(token, 'Token not found')
+
const createAccountReqData = await signedAction<
components['schemas']['CreateAccountRequestData']
>(
{
action: 'createAccount',
email,
- memberId: membership.id,
+ emailConfirmationToken: token.id,
encryptionArtifacts: await prepareEncryptionArtifacts(seed, email, password),
},
keypair
@@ -161,8 +174,22 @@ export async function createAccount(
.set('Cookie', `${SESSION_COOKIE_NAME}=${anonSessionId}`)
.send(createAccountReqData)
.expect(200)
- const account = await em.getRepository(Account).findOneBy({ email })
+ const account = await getAccountByEmail(email)
assert(account, 'Account not found')
+ assert.equal(account.joystreamAccount?.memberships.length, 0)
+
+ // Associate the account with a membership
+ const membership = await insertFakeMember(keypair.address)
+
+ const updatedAccount = await getAccountByEmail(email)
+
+ assert(updatedAccount, 'Account not found')
+ assert.equal(updatedAccount.joystreamAccount.memberships.length, 1)
+ assert.equal(
+ updatedAccount.joystreamAccount.memberships[0].controllerAccountId,
+ membership.controllerAccountId
+ )
+
return { accountId: account.id, joystreamAccountId: keypair.address, email, password, seed }
}
@@ -176,11 +203,13 @@ export async function confirmEmail(token: string, expectedStatus: number): Promi
export async function requestEmailConfirmationToken(
email: string,
+ sessionId: string,
expectedStatus: number
): Promise {
await request(app)
.post('/api/v1/request-email-confirmation-token')
.set('Content-Type', 'application/json')
+ .set('Cookie', `${SESSION_COOKIE_NAME}=${sessionId}`)
.send({ email })
.expect(expectedStatus)
}
diff --git a/src/auth-server/tests/requestEmailConfirmationToken.ts b/src/auth-server/tests/requestEmailConfirmationToken.ts
index 300205b55..5342a0f43 100644
--- a/src/auth-server/tests/requestEmailConfirmationToken.ts
+++ b/src/auth-server/tests/requestEmailConfirmationToken.ts
@@ -1,65 +1,52 @@
-import './config'
-import { globalEm } from '../../utils/globalEm'
-import { EntityManager, MoreThan } from 'typeorm'
-import {
- AccountAccessData,
- confirmEmail,
- createAccount,
- requestEmailConfirmationToken,
-} from './common'
-import { Token, TokenType } from '../../model'
+import { cryptoWaitReady } from '@polkadot/util-crypto'
import assert from 'assert'
+import { EntityManager, MoreThan } from 'typeorm'
+import { EmailConfirmationToken } from '../../model'
import { ConfigVariable, config } from '../../utils/config'
-import { cryptoWaitReady } from '@polkadot/util-crypto'
+import { globalEm } from '../../utils/globalEm'
+import { anonymousAuth, requestEmailConfirmationToken } from './common'
+import './config'
-async function findActiveToken(em: EntityManager, accountId: string): Promise {
- return em.getRepository(Token).findOneBy({
- type: TokenType.EMAIL_CONFIRMATION,
- issuedForId: accountId,
+export async function findActiveToken(
+ em: EntityManager,
+ email: string
+): Promise {
+ return em.getRepository(EmailConfirmationToken).findOneBy({
+ email,
expiry: MoreThan(new Date()),
})
}
-async function reloadToken(em: EntityManager, token: Token): Promise {
- return em.getRepository(Token).findOneByOrFail({ id: token.id })
+async function reloadToken(
+ em: EntityManager,
+ token: EmailConfirmationToken
+): Promise {
+ return em.getRepository(EmailConfirmationToken).findOneByOrFail({ id: token.id })
}
describe('requestEmailConfirmationToken', () => {
let em: EntityManager
- let accountInfo: AccountAccessData
+ const email = `email@example.com`
+ let anonSessionId: string
before(async () => {
await cryptoWaitReady()
em = await globalEm
- accountInfo = await createAccount()
- })
-
- it('should fail if account does not exist', async () => {
- await requestEmailConfirmationToken('non.existing.account@example.com', 404)
+ anonSessionId = await anonymousAuth()
})
- it('should succeed if account exists', async () => {
- await requestEmailConfirmationToken(accountInfo.email, 200)
- const token = await findActiveToken(em, accountInfo.accountId)
+ it('Token should exist', async () => {
+ await requestEmailConfirmationToken(email, anonSessionId, 200)
+ const token = await findActiveToken(em, email)
assert(token, 'Token not found')
})
- it('should fail if account is already confirmed', async () => {
- let oldToken = await findActiveToken(em, accountInfo.accountId)
- assert(oldToken, 'Pre-existing token not found')
- await confirmEmail(oldToken.id, 200)
- oldToken = await reloadToken(em, oldToken)
- assert(oldToken.expiry.getTime() <= Date.now(), 'Pre-existing token is not expired')
- await requestEmailConfirmationToken(accountInfo.email, 400)
- })
-
it('should fail if rate limit is exceeded', async () => {
- const { email, accountId } = await createAccount()
const rateLimit = await config.get(ConfigVariable.EmailConfirmationTokenRateLimit, em)
- let previousToken: Token | null = null
- for (let i = 0; i < rateLimit; i++) {
- await requestEmailConfirmationToken(email, 200)
- const newToken = await findActiveToken(em, accountId)
+ let previousToken: EmailConfirmationToken | null = null
+ for (let i = 1; i < rateLimit; i++) {
+ await requestEmailConfirmationToken(email, anonSessionId, 200)
+ const newToken = await findActiveToken(em, email)
assert(newToken, 'Token not found')
if (previousToken) {
previousToken = await reloadToken(em, previousToken)
@@ -68,6 +55,6 @@ describe('requestEmailConfirmationToken', () => {
}
previousToken = newToken
}
- await requestEmailConfirmationToken(email, 429)
+ await requestEmailConfirmationToken(email, anonSessionId, 429)
})
})
diff --git a/src/auth-server/utils.ts b/src/auth-server/utils.ts
index 949027353..b7a94868c 100644
--- a/src/auth-server/utils.ts
+++ b/src/auth-server/utils.ts
@@ -1,20 +1,30 @@
-import { components } from './generated/api-types'
-import { decodeAddress, cryptoWaitReady, signatureVerify } from '@polkadot/util-crypto'
-import { BadRequestError } from './errors'
-import { config, ConfigVariable } from '../utils/config'
import { JOYSTREAM_ADDRESS_PREFIX } from '@joystream/types'
-import { Account, Token, TokenType } from '../model'
+import {
+ checkAddress,
+ cryptoWaitReady,
+ decodeAddress,
+ signatureVerify,
+} from '@polkadot/util-crypto'
import { EntityManager } from 'typeorm'
+import { EmailConfirmationToken } from '../model'
+import { ConfigVariable, config } from '../utils/config'
import { uniqueId } from '../utils/crypto'
import { sgSendMail } from '../utils/mail'
-import { formatDate } from '../utils/date'
import { registerEmailContent } from './emails'
+import { BadRequestError } from './errors'
+import { components } from './generated/api-types'
export async function verifyActionExecutionRequest(
em: EntityManager,
{ payload, signature }: components['schemas']['ActionExecutionRequestData']
): Promise {
await cryptoWaitReady()
+
+ const [isValid, error] = checkAddress(payload.joystreamAccountId, JOYSTREAM_ADDRESS_PREFIX)
+ if (!isValid) {
+ throw new BadRequestError(`Payload joystreamAccountId is invalid. Error: ${error}`)
+ }
+
const signatureVerifyResult = signatureVerify(
JSON.stringify(payload),
signature,
@@ -45,39 +55,51 @@ export async function verifyActionExecutionRequest(
}
}
-export async function issueEmailConfirmationToken(
- account: Account,
+async function issueEmailConfirmationToken(
+ email: string,
em: EntityManager
-): Promise {
+): Promise {
const issuedAt = new Date()
const lifetimeMs =
(await config.get(ConfigVariable.EmailConfirmationTokenExpiryTimeHours, em)) * 3_600_000
const expiry = new Date(issuedAt.getTime() + lifetimeMs)
- const token = new Token({
+ const token = new EmailConfirmationToken({
id: uniqueId(),
- type: TokenType.EMAIL_CONFIRMATION,
expiry,
issuedAt,
- issuedForId: account.id,
+ email,
})
return em.save(token)
}
-export async function sendWelcomeEmail(account: Account, em: EntityManager): Promise {
- const emailConfirmationToken = await issueEmailConfirmationToken(account, em)
+export async function sendWelcomeEmail(
+ email: string,
+ signupType: string,
+ em: EntityManager
+): Promise {
+ const emailConfirmationToken = await issueEmailConfirmationToken(email, em)
const appName = await config.get(ConfigVariable.AppName, em)
const confirmEmailRoute = await config.get(ConfigVariable.EmailConfirmationRoute, em)
- const emailConfirmationLink = confirmEmailRoute.replace('{token}', emailConfirmationToken.id)
+ const emailConfirmationLink = confirmEmailRoute
+ .replace('{token}', emailConfirmationToken.id)
+ .replace('{expiry}', emailConfirmationToken.expiry.toISOString())
+ .replace('{signupType}', signupType)
const emailContent = registerEmailContent({
link: emailConfirmationLink,
- linkExpiryDate: formatDate(emailConfirmationToken.expiry),
appName,
})
await sgSendMail({
from: await config.get(ConfigVariable.SendgridFromEmail, em),
- to: account.email,
+ to: email,
subject: `Welcome to ${appName}!`,
content: emailContent,
})
}
+
+export async function sendLoginOrChangePasswordEmail(
+ email: string,
+ em: EntityManager
+): Promise {
+ // TODO: implement this
+}
diff --git a/src/mail-scheduler/tests/testUtils.ts b/src/mail-scheduler/tests/testUtils.ts
index 21c292de7..187af83f3 100644
--- a/src/mail-scheduler/tests/testUtils.ts
+++ b/src/mail-scheduler/tests/testUtils.ts
@@ -1,21 +1,21 @@
import {
- Membership,
- User,
- Notification,
Account,
- Unread,
- MemberRecipient,
- NotificationEmailDelivery,
- GatewayConfig,
+ AuctionTypeOpen,
AuctionWon,
EmailDeliveryAttempt,
- AuctionTypeOpen,
+ GatewayConfig,
+ MemberRecipient,
+ Membership,
+ Notification,
+ NotificationEmailDelivery,
+ Unread,
+ User,
} from '../../model'
-import { defaultNotificationPreferences } from '../../utils/notification'
+import { uniqueId } from '../../utils/crypto'
import { globalEm } from '../../utils/globalEm'
import { idStringFromNumber } from '../../utils/misc'
+import { defaultNotificationPreferences } from '../../utils/notification'
import { RUNTIME_NOTIFICATION_ID_TAG } from '../../utils/notification/helpers'
-import { uniqueId } from '../../utils/crypto'
export const NUM_ENTITIES = 3
@@ -33,7 +33,7 @@ export async function populateDbWithSeedData() {
totalChannelsCreated: 0,
handle: `handle-${i}`,
handleRaw: '0x' + Buffer.from(`handle-${i}`).toString('hex'),
- controllerAccount: `j4${i}7rVcUCxi2crhhjRq46fNDRbVHTjJrz6bKxZwehEMQxZeSf`,
+ controllerAccountId: `j4${i}7rVcUCxi2crhhjRq46fNDRbVHTjJrz6bKxZwehEMQxZeSf`,
channels: [],
})
// create users
diff --git a/src/mappings/content/nft.ts b/src/mappings/content/nft.ts
index dfeb5f579..10e96972f 100644
--- a/src/mappings/content/nft.ts
+++ b/src/mappings/content/nft.ts
@@ -44,6 +44,7 @@ import {
createBid,
findTopBid,
finishAuction,
+ getAccountForMember,
getChannelTitleById,
getCurrentAuctionFromVideo,
getNftOwnerMemberId,
@@ -617,9 +618,7 @@ export async function processNftBoughtEvent({
await maybeNotifyNftCreator(overlay, previousNftOwner, notificationData, event)
if (previousNftOwner.isTypeOf === 'NftOwnerMember') {
// case when previous owner is a member
- const previousNftOwnerAccount = await overlay
- .getRepository(Account)
- .getOneByRelation('membershipId', previousNftOwner.member)
+ const previousNftOwnerAccount = await getAccountForMember(overlay, previousNftOwner.member)
await addNotification(
overlay,
previousNftOwnerAccount ? (previousNftOwnerAccount as Account) : null,
diff --git a/src/mappings/content/utils.ts b/src/mappings/content/utils.ts
index ea83f2eae..6c6d98bfb 100644
--- a/src/mappings/content/utils.ts
+++ b/src/mappings/content/utils.ts
@@ -646,21 +646,30 @@ export function encodeAssets(assets: StorageAssetsRecord | undefined): Uint8Arra
export async function getFollowersAccountsForChannel(
overlay: EntityManagerOverlay,
channelId: string
-): Promise {
+): Promise<[string, Account][]> {
const followers = await overlay.getEm().getRepository(ChannelFollow).findBy({ channelId })
const followersUserIds = followers
.filter((follower) => follower?.userId)
.map((follower) => follower.userId as string)
+ const getFollowerAccount = (userId: string) =>
+ overlay
+ .getEm()
+ .getRepository(Account)
+ .findOne({ where: { userId }, relations: { joystreamAccount: { memberships: true } } })
+
const limit = pLimit(10) // Limit to 10 concurrent promises
- const followersAccounts: (Account | null)[] = await Promise.all(
+ const followersAccounts = await Promise.all(
followersUserIds.map((userId) =>
- limit(async () => await overlay.getEm().getRepository(Account).findOneBy({ userId }))
+ limit(async () => {
+ const account = await getFollowerAccount(userId)
+ return [account?.joystreamAccount.memberships[0].id, account] as [string, Account | null]
+ })
)
)
- return followersAccounts.filter((account) => account) as Account[]
+ return followersAccounts.filter(([_, account]) => account !== null) as [string, Account][]
}
export async function getChannelOwnerAccount(
@@ -678,13 +687,29 @@ export async function getAccountForMember(
if (!memberId) {
return null
}
+
// accounts are created by orion_auth_api and updated by orion_graphql-server
const memberAccount = await overlay
.getRepository(Account)
- .getOneByRelation('membershipId', memberId)
+ .getOneByRelation('joystreamAccountId', await getMemberControllerAccount(overlay, memberId))
return (memberAccount as Account) ?? null
}
+async function getMemberControllerAccount(
+ overlay: EntityManagerOverlay,
+ memberId: string
+): Promise {
+ const membership = await overlay.getRepository(Membership).getByIdOrFail(memberId)
+
+ if (!membership.controllerAccountId) {
+ // This should never happen, but only added for type safety as
+ // the foreign entity references are always set nullable by the
+ // subsquid codegen even if in the graphql schema they are not.
+ criticalError(`Membership ${membership.id} controller account not found.`)
+ }
+ return membership.controllerAccountId
+}
+
export async function getAccountsForBidders(
overlay: EntityManagerOverlay,
auctionBids: Flat[]
@@ -753,13 +778,16 @@ export async function notifyChannelFollowers(
) {
const followersAccounts = await getFollowersAccountsForChannel(overlay, channelId)
for (const followerAccount of followersAccounts) {
- await addNotification(
- overlay,
- followerAccount,
- new MemberRecipient({ membership: followerAccount.membershipId }),
- notificationType,
- event
- )
+ // TODO: handling multiple memberships (i.e. follower account address owns multiple memberships). Also, consider a scenario where a follower account has no memberships.
+ for (const membership of followerAccount.joystreamAccount.memberships) {
+ await addNotification(
+ overlay,
+ followerAccount,
+ new MemberRecipient({ membership: membership.id }),
+ notificationType,
+ event
+ )
+ }
}
}
diff --git a/src/mappings/membership/index.ts b/src/mappings/membership/index.ts
index 63bcd1a47..399ba50a6 100644
--- a/src/mappings/membership/index.ts
+++ b/src/mappings/membership/index.ts
@@ -1,14 +1,15 @@
+import { MemberRemarked, MembershipMetadata } from '@joystream/metadata-protobuf'
import { u8aToHex } from '@polkadot/util'
import {
+ BlockchainAccount,
Event,
Membership,
MetaprotocolTransactionResultFailed,
MetaprotocolTransactionStatusEventData,
} from '../../model'
import { EventHandlerContext } from '../../utils/events'
-import { MemberRemarked, MembershipMetadata } from '@joystream/metadata-protobuf'
import { bytesToString, deserializeMetadata, genericEventFields, toAddress } from '../utils'
-import { processMembershipMetadata, processMemberRemark } from './metadata'
+import { processMemberRemark, processMembershipMetadata } from './metadata'
export async function processNewMember({
overlay,
@@ -24,10 +25,19 @@ export async function processNewMember({
const { controllerAccount, handle: handleBytes, metadata: metadataBytes } = params
const metadata = deserializeMetadata(MembershipMetadata, metadataBytes)
+ const controllerAccountId = toAddress(controllerAccount)
+
+ // Create blockchain account entity if it doesn't exist
+ const blockchainAccount =
+ (await overlay.getRepository(BlockchainAccount).getById(controllerAccountId)) ||
+ overlay.getRepository(BlockchainAccount).new({
+ id: controllerAccountId,
+ })
+
const member = overlay.getRepository(Membership).new({
createdAt: new Date(block.timestamp),
id: memberId.toString(),
- controllerAccount: toAddress(controllerAccount),
+ controllerAccountId: blockchainAccount.id,
totalChannelsCreated: 0,
})
if (handleBytes) {
@@ -46,8 +56,15 @@ export async function processMemberAccountsUpdatedEvent({
},
}: EventHandlerContext<'Members.MemberAccountsUpdated'>) {
if (newControllerAccount) {
+ // Create blockchain account entity if it doesn't exist
+ const controllerAccountId = toAddress(newControllerAccount)
+ const blockchainAccount =
+ (await overlay.getRepository(BlockchainAccount).getById(controllerAccountId)) ||
+ overlay.getRepository(BlockchainAccount).new({
+ id: controllerAccountId,
+ })
const member = await overlay.getRepository(Membership).getByIdOrFail(memberId.toString())
- member.controllerAccount = toAddress(newControllerAccount)
+ member.controllerAccountId = blockchainAccount.id
}
}
diff --git a/src/mappings/token/utils.ts b/src/mappings/token/utils.ts
index 32c5f8a9c..c4183b593 100644
--- a/src/mappings/token/utils.ts
+++ b/src/mappings/token/utils.ts
@@ -23,7 +23,7 @@ import { uniqueId } from '../../utils/crypto'
import { criticalError } from '../../utils/misc'
import { addNotification } from '../../utils/notification'
import { EntityManagerOverlay, Flat } from '../../utils/overlay'
-import { getFollowersAccountsForChannel } from '../content/utils'
+import { getAccountForMember, getFollowersAccountsForChannel } from '../content/utils'
export const FALLBACK_TOKEN_SYMBOL = '??'
@@ -320,10 +320,13 @@ export async function processTokenMetadata(
}
}
+/**
+ * @returns [holderMemberId, Account][]
+ */
export async function getHolderAccountsForToken(
overlay: EntityManagerOverlay,
tokenId: string
-): Promise {
+): Promise<[string, Account][]> {
const em = overlay.getEm()
const holders = await em.getRepository(TokenAccount).findBy({ tokenId })
@@ -332,13 +335,16 @@ export async function getHolderAccountsForToken(
.map((holder) => holder.memberId as string)
const limit = pLimit(10) // Limit to 10 concurrent promises
- const holdersAccounts: (Account | null)[] = await Promise.all(
+ const holdersAccounts = await Promise.all(
holdersMemberIds.map((membershipId) =>
- limit(async () => await em.getRepository(Account).findOneBy({ membershipId }))
+ limit(async () => {
+ const account = await getAccountForMember(overlay, membershipId)
+ return [membershipId, account] as [string, Account | null]
+ })
)
)
- return holdersAccounts.filter((account) => account) as Account[]
+ return holdersAccounts.filter(([_, account]) => account !== null) as [string, Account][]
}
export async function notifyTokenHolders(
@@ -353,12 +359,12 @@ export async function notifyTokenHolders(
const limit = pLimit(10) // Limit to 10 concurrent promises
await Promise.all(
- holderAccounts.map((holderAccount) =>
+ holderAccounts.map(([holderMemberId, holderAccount]) =>
limit(() =>
addNotification(
em,
holderAccount,
- new MemberRecipient({ membership: holderAccount.membershipId }),
+ new MemberRecipient({ membership: holderMemberId }),
notificationType,
event,
dispatchBlock
diff --git a/src/mappings/utils.ts b/src/mappings/utils.ts
index 4f2da75f3..6b1d05fd8 100644
--- a/src/mappings/utils.ts
+++ b/src/mappings/utils.ts
@@ -5,8 +5,16 @@ import { Bytes } from '@polkadot/types/primitive'
import { u8aToHex } from '@polkadot/util'
import { encodeAddress } from '@polkadot/util-crypto'
import { SubstrateBlock } from '@subsquid/substrate-processor'
+import { EntityManager } from 'typeorm'
import { Logger } from '../logger'
-import { Event, MetaprotocolTransactionResultFailed, NftActivity, NftHistoryEntry } from '../model'
+import {
+ Account,
+ Event,
+ Membership,
+ MetaprotocolTransactionResultFailed,
+ NftActivity,
+ NftHistoryEntry,
+} from '../model'
import { CommentCountersManager } from '../utils/CommentsCountersManager'
import { VideoRelevanceManager } from '../utils/VideoRelevanceManager'
import { EntityManagerOverlay } from '../utils/overlay'
@@ -106,6 +114,33 @@ export function addNftActivity(
}
}
+export async function getAccountForMemberOrFail(
+ em: EntityManager,
+ memberOrMemberId: string | Membership
+) {
+ const account = await getAccountForMember(em, memberOrMemberId)
+
+ if (!account) {
+ throw new Error(`Account not found for member ${memberOrMemberId}`)
+ }
+ return account
+}
+
+export async function getAccountForMember(
+ em: EntityManager,
+ memberOrMemberId: string | Membership
+): Promise {
+ const member =
+ memberOrMemberId instanceof Membership
+ ? memberOrMemberId
+ : await em.getRepository(Membership).findOneByOrFail({ id: memberOrMemberId })
+
+ return em.getRepository(Account).findOne({
+ where: { joystreamAccountId: member.controllerAccountId! },
+ relations: { joystreamAccount: { memberships: true } },
+ })
+}
+
export function toAddress(addressBytes: Uint8Array) {
return encodeAddress(addressBytes, JOYSTREAM_SS58_PREFIX)
}
diff --git a/src/server-extension/check.ts b/src/server-extension/check.ts
index 3c16c11dd..ae219b91b 100644
--- a/src/server-extension/check.ts
+++ b/src/server-extension/check.ts
@@ -2,8 +2,10 @@ import { RequestCheckFunction } from '@subsquid/graphql-server/lib/check'
import { TypeormOpenreaderContext } from '@subsquid/graphql-server/lib/typeorm'
import { Context as OpenreaderContext } from '@subsquid/openreader/lib/context'
import { UnauthorizedError } from 'type-graphql'
-import { AuthContext, authenticate } from '../utils/auth'
+import { Session } from '../model'
+import { authenticate } from '../utils/auth'
+type AuthContext = Session | null
export type Context = OpenreaderContext & AuthContext
const autogeneratedOperatorQueries = [
diff --git a/src/server-extension/resolvers/AccountResolver/index.ts b/src/server-extension/resolvers/AccountResolver/index.ts
index 01796a410..c286d5442 100644
--- a/src/server-extension/resolvers/AccountResolver/index.ts
+++ b/src/server-extension/resolvers/AccountResolver/index.ts
@@ -1,13 +1,24 @@
+import assert from 'assert'
+import axios from 'axios'
+import { GraphQLResolveInfo } from 'graphql'
import 'reflect-metadata'
-import { Query, Resolver, UseMiddleware, Ctx, Info } from 'type-graphql'
+import { Args, Ctx, Info, Mutation, Query, Resolver, UseMiddleware } from 'type-graphql'
import { EntityManager } from 'typeorm'
-import { AccountOnly } from '../middleware'
-import { AccountData, FollowedChannel } from './types'
-import { Context } from '../../check'
-import { GraphQLResolveInfo } from 'graphql'
-import assert from 'assert'
+import { Account, ChannelFollow } from '../../../model'
+import { getCurrentBlockHeight } from '../../../utils/blockHeight'
+import { ConfigVariable, config } from '../../../utils/config'
+import { pWaitFor } from '../../../utils/misc'
import { withHiddenEntities } from '../../../utils/sql'
-import { ChannelFollow } from '../../../model'
+import { Context } from '../../check'
+import { AccountOnly } from '../middleware'
+import {
+ AccountData,
+ CreateAccountMembershipArgs,
+ CreateAccountMembershipResult,
+ FaucetRegisterMembershipParams,
+ FaucetRegisterMembershipResponse,
+ FollowedChannel,
+} from './types'
@Resolver()
export class AccountResolver {
@@ -20,33 +31,104 @@ export class AccountResolver {
const account = ctx.account
const em = await this.em()
assert(account, 'Unexpected context: account is not set')
- const { id, email, joystreamAccount, membershipId, isEmailConfirmed, notificationPreferences } =
- account
+ const { id, email, joystreamAccountId, notificationPreferences } = account
let followedChannels: FollowedChannel[] = []
if (
info.fieldNodes[0].selectionSet?.selections.some(
(s) => s.kind === 'Field' && s.name.value === 'followedChannels'
)
) {
- followedChannels = await withHiddenEntities(em, async () => {
- const followedChannels = await em
- .getRepository(ChannelFollow)
- .findBy({ userId: account.userId })
- return followedChannels.map(({ channelId, timestamp }) => ({
- channelId,
- timestamp: timestamp.toISOString(),
- }))
- })
+ followedChannels = await this.getFollowedChannels(id)
}
return {
id,
email,
- joystreamAccount,
- membershipId,
- isEmailConfirmed,
+ joystreamAccountId,
followedChannels,
preferences: notificationPreferences,
}
}
+
+ @UseMiddleware(AccountOnly)
+ @Mutation(() => CreateAccountMembershipResult)
+ async createAccountMembership(
+ @Args() args: CreateAccountMembershipArgs,
+ @Ctx() ctx: Context
+ ): Promise {
+ const em = await this.em()
+ return em.transaction(async (em) => {
+ const account = await em.getRepository(Account).findOne({
+ where: { id: ctx.account?.id },
+ lock: { mode: 'pessimistic_write' },
+ relations: { joystreamAccount: { memberships: true } },
+ })
+
+ if (!account) {
+ throw new Error('Account not found')
+ }
+
+ if (account.joystreamAccount.memberships.length > 0) {
+ throw new Error('Membership already exists')
+ }
+
+ const { handle, avatar, about, name } = args
+ const address = account.joystreamAccountId
+
+ const { memberId, block } = await this.createMemberWithFaucet({
+ address,
+ handle,
+ avatar,
+ about,
+ name,
+ })
+
+ // ensure membership is processed
+ await pWaitFor(
+ async () => (await getCurrentBlockHeight(em)).lastProcessedBlock >= block,
+ 1000, // 1 second
+ 20000, // 20 seconds
+ 'Membership not processed'
+ )
+
+ return { accountId: account.id, memberId }
+ })
+ }
+
+ private async getFollowedChannels(accountId: string): Promise {
+ const em = await this.em()
+ return withHiddenEntities(em, async () => {
+ const followedChannels = await em.getRepository(ChannelFollow).findBy({ userId: accountId })
+ return followedChannels.map(({ channelId, timestamp }) => ({
+ channelId,
+ timestamp: timestamp.toISOString(),
+ }))
+ })
+ }
+
+ private async createMemberWithFaucet(
+ params: FaucetRegisterMembershipParams
+ ): Promise {
+ const em = await this.em()
+ const url = await config.get(ConfigVariable.FaucetUrl, em)
+ const captchaBypassKey = await config.get(ConfigVariable.FaucetCaptchaBypassKey, em)
+
+ try {
+ const response = await axios.post(url, params, {
+ headers: { Authorization: `Bearer ${captchaBypassKey}` },
+ })
+ return response.data
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ throw new Error(
+ `Failed to create membership through faucet for account address: ${
+ params.address
+ }, error: ${
+ error.response?.data?.error || error.cause || error.code
+ }, faucet address: ${url}`
+ )
+ }
+ throw error
+ }
+ }
}
diff --git a/src/server-extension/resolvers/AccountResolver/types.ts b/src/server-extension/resolvers/AccountResolver/types.ts
index 9d3c0918a..ab5c767b7 100644
--- a/src/server-extension/resolvers/AccountResolver/types.ts
+++ b/src/server-extension/resolvers/AccountResolver/types.ts
@@ -1,4 +1,5 @@
-import { Field, ObjectType } from 'type-graphql'
+import { IsUrl } from 'class-validator'
+import { ArgsType, Field, ObjectType } from 'type-graphql'
import { AccountNotificationPreferencesOutput } from '../NotificationResolver/types'
@ObjectType()
@@ -19,13 +20,7 @@ export class AccountData {
email!: string
@Field(() => String, { nullable: false })
- joystreamAccount!: string
-
- @Field(() => Boolean, { nullable: false })
- isEmailConfirmed!: boolean
-
- @Field(() => String, { nullable: false })
- membershipId: string
+ joystreamAccountId!: string
@Field(() => [FollowedChannel], { nullable: false })
followedChannels: FollowedChannel[]
@@ -33,3 +28,41 @@ export class AccountData {
@Field(() => AccountNotificationPreferencesOutput, { nullable: true })
preferences?: AccountNotificationPreferencesOutput
}
+
+@ArgsType()
+export class CreateAccountMembershipArgs {
+ @Field(() => String, { nullable: false, description: 'Membership Handle' })
+ handle: string
+
+ @Field(() => String, { description: 'Membership avatar URL' })
+ @IsUrl({ require_tld: false })
+ avatar: string
+
+ @Field(() => String, { description: '`about` information to associate with new Membership' })
+ about: string
+
+ @Field(() => String, { description: 'Membership name' })
+ name: string
+}
+
+@ObjectType()
+export class CreateAccountMembershipResult {
+ @Field(() => String, { nullable: false })
+ accountId!: string
+
+ @Field(() => Number, { nullable: false })
+ memberId: number
+}
+
+export type FaucetRegisterMembershipParams = {
+ address: string
+ handle: string
+ avatar: string
+ about: string
+ name: string
+}
+
+export type FaucetRegisterMembershipResponse = {
+ memberId: number
+ block: number
+}
diff --git a/src/server-extension/resolvers/AdminResolver/index.ts b/src/server-extension/resolvers/AdminResolver/index.ts
index 6dfcea50f..5e5ba624c 100644
--- a/src/server-extension/resolvers/AdminResolver/index.ts
+++ b/src/server-extension/resolvers/AdminResolver/index.ts
@@ -12,9 +12,8 @@ import 'reflect-metadata'
import { Args, Ctx, Info, Int, Mutation, Query, Resolver, UseMiddleware } from 'type-graphql'
import { EntityManager, In, Not, UpdateResult } from 'typeorm'
import { parseVideoTitle } from '../../../mappings/content/utils'
-import { videoRelevanceManager } from '../../../mappings/utils'
+import { getAccountForMember, videoRelevanceManager } from '../../../mappings/utils'
import {
- Account,
Channel,
ChannelRecipient,
CreatorToken,
@@ -548,9 +547,7 @@ export const setFeaturedNftsInner = async (em: EntityManager, featuredNftsIds: s
videoTitle: parseVideoTitle(featuredNft.video),
}
const channelOwnerAccount = featuredNft.video.channel.ownerMemberId
- ? await em
- .getRepository(Account)
- .findOneBy({ membershipId: featuredNft.video.channel.ownerMemberId })
+ ? await getAccountForMember(em, featuredNft.video.channel.ownerMemberId)
: null
await addNotification(
em,
diff --git a/src/server-extension/resolvers/ChannelsResolver/index.ts b/src/server-extension/resolvers/ChannelsResolver/index.ts
index 06ab2a602..5f0e631d1 100644
--- a/src/server-extension/resolvers/ChannelsResolver/index.ts
+++ b/src/server-extension/resolvers/ChannelsResolver/index.ts
@@ -1,60 +1,60 @@
+import { parseAnyTree } from '@subsquid/openreader/lib/opencrud/tree'
+import { ListQuery } from '@subsquid/openreader/lib/sql/query'
+import { getResolveTree } from '@subsquid/openreader/lib/util/resolve-tree'
+import { assertNotNull } from '@subsquid/substrate-processor'
+import { GraphQLResolveInfo } from 'graphql'
+import pLimit from 'p-limit'
import 'reflect-metadata'
-import { Args, Query, Mutation, Resolver, Info, Ctx, UseMiddleware } from 'type-graphql'
+import { Args, Ctx, Info, Mutation, Query, Resolver, UseMiddleware } from 'type-graphql'
import { EntityManager, In, IsNull } from 'typeorm'
+import { FALLBACK_CHANNEL_TITLE } from '../../../mappings/content/utils'
+import { getAccountForMember } from '../../../mappings/utils'
+import {
+ Channel,
+ ChannelExcluded,
+ ChannelFollow,
+ ChannelRecipient,
+ ChannelSuspended,
+ ChannelSuspension,
+ ChannelVerification,
+ ChannelVerified,
+ Exclusion,
+ MemberRecipient,
+ Membership,
+ NewChannelFollower,
+ Report,
+ YppSuspended,
+ YppVerified,
+} from '../../../model'
+import { uniqueId } from '../../../utils/crypto'
+import { addNotification } from '../../../utils/notification'
+import { extendClause, withHiddenEntities } from '../../../utils/sql'
+import { Context } from '../../check'
+import { AccountOnly, OperatorOnly, UserOnly } from '../middleware'
+import { model } from '../model'
import {
+ ChannelFollowResult,
ChannelNftCollector,
ChannelNftCollectorsArgs,
+ ChannelNftCollectorsOrderByInput,
+ ChannelReportInfo,
+ ChannelUnfollowResult,
+ ExcludeChannelArgs,
+ ExcludeChannelResult,
ExtendedChannel,
ExtendedChannelsArgs,
FollowChannelArgs,
- UnfollowChannelArgs,
MostRecentChannelsArgs,
- ChannelReportInfo,
ReportChannelArgs,
- ChannelFollowResult,
- ChannelUnfollowResult,
- ChannelNftCollectorsOrderByInput,
+ SuspendChannelArgs,
+ SuspendChannelResult,
TopSellingChannelsArgs,
TopSellingChannelsResult,
- ExcludeChannelArgs,
- ExcludeChannelResult,
+ UnfollowChannelArgs,
VerifyChannelArgs,
VerifyChannelResult,
- SuspendChannelResult,
- SuspendChannelArgs,
} from './types'
-import { GraphQLResolveInfo } from 'graphql'
-import {
- Account,
- Channel,
- ChannelFollow,
- Report,
- Membership,
- Exclusion,
- NewChannelFollower,
- ChannelExcluded,
- ChannelVerified,
- ChannelVerification,
- ChannelSuspension,
- ChannelSuspended,
- YppVerified,
- YppSuspended,
- ChannelRecipient,
- MemberRecipient,
-} from '../../../model'
-import { extendClause, withHiddenEntities } from '../../../utils/sql'
import { buildExtendedChannelsQuery, buildTopSellingChannelsQuery } from './utils'
-import { parseAnyTree } from '@subsquid/openreader/lib/opencrud/tree'
-import { getResolveTree } from '@subsquid/openreader/lib/util/resolve-tree'
-import { ListQuery } from '@subsquid/openreader/lib/sql/query'
-import { model } from '../model'
-import { Context } from '../../check'
-import { uniqueId } from '../../../utils/crypto'
-import { AccountOnly, OperatorOnly, UserOnly } from '../middleware'
-import { addNotification } from '../../../utils/notification'
-import { assertNotNull } from '@subsquid/substrate-processor'
-import { FALLBACK_CHANNEL_TITLE } from '../../../mappings/content/utils'
-import pLimit from 'p-limit'
@Resolver()
export class ChannelsResolver {
@@ -224,7 +224,7 @@ export class ChannelsResolver {
})
const ownerAccount = channel.ownerMemberId
- ? await em.getRepository(Account).findOneBy({ membershipId: channel.ownerMemberId })
+ ? await getAccountForMember(em, channel.ownerMemberId)
: null
if (ownerAccount) {
if (!ctx.account) {
@@ -233,7 +233,7 @@ export class ChannelsResolver {
}
const followerMembership = await em
.getRepository(Membership)
- .findOneByOrFail({ id: ctx.account.membershipId })
+ .findOneByOrFail({ controllerAccountId: ctx.account.joystreamAccountId })
await addNotification(
em,
ownerAccount,
@@ -376,9 +376,7 @@ export class ChannelsResolver {
// in case account exist deposit notification
const channelOwnerMemberId = channel.ownerMemberId
if (channelOwnerMemberId) {
- const account = await em.findOne(Account, {
- where: { membershipId: channelOwnerMemberId },
- })
+ const account = await getAccountForMember(em, channelOwnerMemberId)
await addNotification(
em,
account,
@@ -458,7 +456,7 @@ export const excludeChannelService = async (
// in case account exist deposit notification
const channelOwnerMemberId = channel.ownerMemberId
if (channelOwnerMemberId) {
- const account = await em.findOne(Account, { where: { membershipId: channelOwnerMemberId } })
+ const account = await getAccountForMember(em, channelOwnerMemberId)
await addNotification(
em,
account,
@@ -508,9 +506,7 @@ export const verifyChannelService = async (em: EntityManager, channelIds: string
// in case account exist deposit notification
const channelOwnerMemberId = channel.ownerMemberId
if (channelOwnerMemberId) {
- const account = await em.findOne(Account, {
- where: { membershipId: channelOwnerMemberId },
- })
+ const account = await getAccountForMember(em, channelOwnerMemberId)
await addNotification(
em,
account,
diff --git a/src/server-extension/resolvers/VideosResolver/index.ts b/src/server-extension/resolvers/VideosResolver/index.ts
index 63b0a847d..47d537fb9 100644
--- a/src/server-extension/resolvers/VideosResolver/index.ts
+++ b/src/server-extension/resolvers/VideosResolver/index.ts
@@ -22,9 +22,8 @@ import 'reflect-metadata'
import { Arg, Args, Ctx, Info, Mutation, Query, Resolver, UseMiddleware } from 'type-graphql'
import { EntityManager, In, MoreThan } from 'typeorm'
import { parseVideoTitle } from '../../../mappings/content/utils'
-import { videoRelevanceManager } from '../../../mappings/utils'
+import { getAccountForMember, videoRelevanceManager } from '../../../mappings/utils'
import {
- Account,
ChannelRecipient,
Exclusion,
OperatorPermission,
@@ -424,7 +423,7 @@ export const excludeVideoService = async (
// in case account exist deposit notification
const channelOwnerMemberId = video.channel.ownerMemberId
if (channelOwnerMemberId) {
- const account = await em.findOne(Account, { where: { membershipId: channelOwnerMemberId } })
+ const account = await getAccountForMember(em, channelOwnerMemberId)
await addNotification(
em,
account,
diff --git a/src/server-extension/resolvers/middleware.ts b/src/server-extension/resolvers/middleware.ts
index b4dc09639..37efe8c85 100644
--- a/src/server-extension/resolvers/middleware.ts
+++ b/src/server-extension/resolvers/middleware.ts
@@ -7,7 +7,7 @@ export const OperatorOnly = (
): MiddlewareFn => {
return async ({ context }, next) => {
// Ensure the user exists in the context
- if (!context?.user) {
+ if (!context.user) {
throw new Error('Unauthorized: User required')
}
diff --git a/src/tests/integration/notifications.test.ts b/src/tests/integration/notifications.test.ts
index 6ff931146..1c9a5b8bb 100644
--- a/src/tests/integration/notifications.test.ts
+++ b/src/tests/integration/notifications.test.ts
@@ -8,9 +8,8 @@ import path from 'path'
import { EntityManager } from 'typeorm'
import { auctionBidMadeInner } from '../../mappings/content/nft'
import { processMemberRemarkedEvent } from '../../mappings/membership'
-import { backwardCompatibleMetaID } from '../../mappings/utils'
+import { backwardCompatibleMetaID, getAccountForMember } from '../../mappings/utils'
import {
- Account,
Channel,
ChannelExcluded,
ChannelRecipient,
@@ -95,9 +94,7 @@ describe('notifications tests', () => {
})
const channel = await em.getRepository(Channel).findOneByOrFail({ id: channelId })
const nextNotificationIdPost = await getNextNotificationId(em, false)
- const account = await em
- .getRepository(Account)
- .findOneBy({ membershipId: channel!.ownerMemberId! })
+ const account = await getAccountForMember(em, channel!.ownerMemberId!)
expect(notification).not.to.be.null
expect(channel).not.to.be.null
expect(notification!.notificationType.isTypeOf).to.equal('ChannelVerified')
@@ -143,9 +140,7 @@ describe('notifications tests', () => {
})
const channel = await em.getRepository(Channel).findOneBy({ id: channelId })
const nextNotificationIdPost = await getNextNotificationId(em, false)
- const account = await em
- .getRepository(Account)
- .findOneBy({ membershipId: channel!.ownerMemberId! })
+ const account = await getAccountForMember(em, channel!.ownerMemberId!)
expect(notification).not.to.be.null
expect(channel).not.to.be.null
expect(notification!.notificationType.isTypeOf).to.equal('ChannelExcluded')
@@ -202,9 +197,7 @@ describe('notifications tests', () => {
expect(video).not.to.be.null
expect(video!.channel).not.to.be.null
const nextNotificationIdPost = await getNextNotificationId(em, false)
- const account = await em
- .getRepository(Account)
- .findOneBy({ membershipId: video!.channel.ownerMemberId! })
+ const account = await getAccountForMember(em, video!.channel!.ownerMemberId!)
expect(notification).not.to.be.null
expect(notification!.notificationType.isTypeOf).to.equal('VideoExcluded')
expect(notification!.status.isTypeOf).to.equal('Unread')
@@ -251,9 +244,7 @@ describe('notifications tests', () => {
const nft = await em
.getRepository(OwnedNft)
.findOneOrFail({ where: { id: nftId }, relations: { video: { channel: true } } })
- const account = await em
- .getRepository(Account)
- .findOneBy({ membershipId: nft!.video!.channel!.ownerMemberId! })
+ const account = await getAccountForMember(em, nft!.video!.channel!.ownerMemberId!)
const nextNotificationIdPost = await getNextNotificationId(em, false)
expect(notification).not.to.be.null
expect(notification!.notificationType.isTypeOf).to.equal('NftFeaturedOnMarketPlace')
@@ -310,9 +301,7 @@ describe('notifications tests', () => {
notification = (await overlay
.getRepository(Notification)
.getById(notificationId)) as Notification | null
- const account = (await overlay
- .getRepository(Account)
- .getOneByRelationOrFail('membershipId', outbiddedMember)) as Account
+ const account = await getAccountForMember(em, outbiddedMember)
expect(notification).not.to.be.null
expect(notification!.notificationType.isTypeOf).to.equal('HigherBidPlaced')
@@ -338,9 +327,7 @@ describe('notifications tests', () => {
notification = (await overlay
.getRepository(Notification)
.getByIdOrFail(notificationId)) as Notification
- const account = await overlay
- .getRepository(Account)
- .getOneByRelationOrFail('membershipId', channel!.ownerMemberId!)
+ const account = await getAccountForMember(em, channel!.ownerMemberId!)
// complete the missing checks as above
expect(notification).not.to.be.null
diff --git a/src/tests/integration/seedData.test.ts b/src/tests/integration/seedData.test.ts
index 536807c23..ee38bd163 100644
--- a/src/tests/integration/seedData.test.ts
+++ b/src/tests/integration/seedData.test.ts
@@ -1,8 +1,7 @@
import { expect } from 'chai'
+import { EntityManager } from 'typeorm'
import { Account, Auction, AuctionTypeEnglish, Channel, OwnedNft } from '../../model'
import { globalEm } from '../../utils/globalEm'
-import { EntityManager } from 'typeorm'
-import _ from 'lodash'
import { clearDb, populateDbWithSeedData } from './testUtils'
describe('Database seed data tests', () => {
@@ -19,9 +18,9 @@ describe('Database seed data tests', () => {
it('check that seed membership data exists', async () => {
const result = await em
.getRepository(Account)
- .findOne({ where: { id: '1' }, relations: { membership: true } })
+ .findOne({ where: { id: '1' }, relations: { joystreamAccount: { memberships: true } } })
expect(result).to.not.be.null
- expect(result?.membership.id).to.equal('1')
+ expect(result?.joystreamAccount.memberships[0].id).to.equal('1')
})
describe('check that seed content entities exists', () => {
diff --git a/src/tests/integration/testUtils.ts b/src/tests/integration/testUtils.ts
index 799476ece..44d22e780 100644
--- a/src/tests/integration/testUtils.ts
+++ b/src/tests/integration/testUtils.ts
@@ -5,6 +5,7 @@ import {
Auction,
AuctionTypeEnglish,
Bid,
+ BlockchainAccount,
Channel,
Membership,
NftOwnerChannel,
@@ -24,30 +25,35 @@ import { defaultNotificationPreferences } from '../../utils/notification'
export async function populateDbWithSeedData() {
const em = await globalEm
for (let i = 0; i < 10; i++) {
+ const controllerAccountId = `controller-account-${i}`
+ const blockchainAccount = new BlockchainAccount({
+ id: controllerAccountId,
+ })
+ await em.save(blockchainAccount)
const member = new Membership({
createdAt: new Date(),
id: i.toString(),
- controllerAccount: `controller-account-${i}`,
+ controllerAccountId,
handle: `handle-${i}`,
handleRaw: '0x' + Buffer.from(`handle-${i}`).toString('hex'),
totalChannelsCreated: 0,
})
+ await em.save(member)
const user = new User({
id: `user-${i}`,
isRoot: false,
})
+ await em.save(user)
const account = new Account({
id: i.toString(),
email: `test-email-${i}@example.com`,
- isEmailConfirmed: false,
registeredAt: new Date(),
isBlocked: false,
userId: user.id,
- joystreamAccount: `test-joystream-account-${i}`,
- membershipId: member.id,
+ joystreamAccountId: controllerAccountId,
notificationPreferences: defaultNotificationPreferences(),
})
- await em.save([user, member, account])
+ await em.save(account)
}
for (let i = 0; i < 10; i++) {
const channel = new Channel({
diff --git a/src/tests/migrations/migration.test.ts b/src/tests/migrations/migration.test.ts
index 4f11e9610..704f53fa8 100644
--- a/src/tests/migrations/migration.test.ts
+++ b/src/tests/migrations/migration.test.ts
@@ -1,17 +1,15 @@
+import { expect } from 'chai'
import { EntityManager } from 'typeorm'
-import { globalEm } from '../../utils/globalEm'
+import { getAccountForMemberOrFail } from '../../mappings/utils'
import { Account, Channel, NextEntityId, NotificationPreference } from '../../model'
-import { expect } from 'chai'
+import { globalEm } from '../../utils/globalEm'
const arePrefsAllTrue = (account: Account) =>
Object.values(account.notificationPreferences).every((v: NotificationPreference) => {
return v.inAppEnabled && v.emailEnabled
})
const queryAccount = async (membershipId: string, em: EntityManager) => {
- return await em.getRepository(Account).findOneOrFail({
- where: { membershipId },
- relations: { notifications: true },
- })
+ return await getAccountForMemberOrFail(em, membershipId)
}
describe('Migration from 3.0.2 to 3.2.0', () => {
@@ -36,10 +34,6 @@ describe('Migration from 3.0.2 to 3.2.0', () => {
expect(aliceAccount.notifications).to.have.lengthOf(0)
expect(bobAccount.notifications).to.have.lengthOf(0)
})
- it('referrer channel id should be null', () => {
- expect(aliceAccount.referrerChannelId).to.be.null
- expect(bobAccount.referrerChannelId).to.be.null
- })
})
describe('Channels are migrated properly', () => {
const aliceChannelId = '1'
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
index 9484cf599..2c65c8b92 100644
--- a/src/utils/auth.ts
+++ b/src/utils/auth.ts
@@ -105,15 +105,13 @@ sessionCache.on('expired', (sessionId: string, cachedData: CachedSessionData) =>
})
})
-export type AuthContext = Session | null
-
-export async function getSessionIdFromHeader(req: Request): Promise {
+export function getSessionIdFromHeader(req: Request): string | undefined {
authLogger.trace(`Authorization header: ${JSON.stringify(req.headers.authorization, null, 2)}`)
const [, sessionId] = req.headers.authorization?.match(/^Bearer ([A-Za-z0-9+/=]+)$/) || []
return sessionId
}
-export async function getSessionIdFromCookie(req: Request): Promise {
+export function getSessionIdFromCookie(req: Request): string | undefined {
authLogger.trace(`Cookies: ${JSON.stringify(req.cookies, null, 2)}`)
return req.cookies ? req.cookies[SESSION_COOKIE_NAME] : undefined
}
@@ -121,10 +119,10 @@ export async function getSessionIdFromCookie(req: Request): Promise {
+): Promise {
const em = await globalEm
const sessionId =
- authType === 'cookie' ? await getSessionIdFromCookie(req) : await getSessionIdFromHeader(req)
+ authType === 'cookie' ? getSessionIdFromCookie(req) : getSessionIdFromHeader(req)
if (sessionId) {
authLogger.trace(`Authenticating... SessionId: ${sessionId}`)
diff --git a/src/utils/config.ts b/src/utils/config.ts
index e91ea67b7..5734d8241 100644
--- a/src/utils/config.ts
+++ b/src/utils/config.ts
@@ -24,6 +24,8 @@ export enum ConfigVariable {
AppAssetStorage = 'APP_ASSET_STORAGE',
AppNameAlt = 'APP_NAME_ALT',
NotificationAssetRoot = 'NOTIFICATION_ASSET_ROOT',
+ FaucetUrl = 'FAUCET_URL',
+ FaucetCaptchaBypassKey = 'FAUCET_CAPTCHA_BYPASS_KEY',
}
const boolType = {
@@ -69,6 +71,8 @@ export const configVariables = {
[ConfigVariable.AppAssetStorage]: stringType,
[ConfigVariable.AppNameAlt]: stringType,
[ConfigVariable.NotificationAssetRoot]: stringType,
+ [ConfigVariable.FaucetUrl]: stringType,
+ [ConfigVariable.FaucetCaptchaBypassKey]: stringType,
} as const
type TypeOf = ReturnType<(typeof configVariables)[C]['deserialize']>
diff --git a/src/utils/mail.ts b/src/utils/mail.ts
index 6189692f2..60f1162e4 100644
--- a/src/utils/mail.ts
+++ b/src/utils/mail.ts
@@ -24,13 +24,15 @@ export async function sgSendMail({ from, to, subject, content }: SendMailArgs) {
)
return
}
+
const [clientResponse] = await sgMail.send({
from,
to,
subject,
html: content,
})
- // mailerLogger.info(`E-mail sent:\n${JSON.stringify({ from, to, subject, content }, null, 2)}`)
+
+ mailerLogger.info(`E-mail sent:\n${JSON.stringify({ from, to, subject, content }, null, 2)}`)
return clientResponse
}
diff --git a/src/utils/misc.ts b/src/utils/misc.ts
index e7de05dc5..96083b55c 100644
--- a/src/utils/misc.ts
+++ b/src/utils/misc.ts
@@ -30,3 +30,33 @@ export function idStringFromNumber(idNum: number) {
// Add leading zeros to simplify sorting
return _.repeat('0', 8 - idStr.length) + idStr
}
+
+export async function pWaitFor(
+ conditionFunction: () => Promise,
+ checkInterval = 100,
+ timeout = 5000, // Default timeout of 5000 milliseconds
+ errorMessage = 'Condition timed out'
+): Promise {
+ return new Promise((resolve, reject) => {
+ let elapsedTime = 0
+
+ const interval = setInterval(() => {
+ conditionFunction()
+ .then((isConditionMet) => {
+ if (isConditionMet) {
+ clearInterval(interval)
+ resolve()
+ } else if (elapsedTime > timeout) {
+ clearInterval(interval)
+ reject(new Error(errorMessage))
+ } else {
+ elapsedTime += checkInterval // Update the elapsed time
+ }
+ })
+ .catch((error) => {
+ clearInterval(interval)
+ reject(error)
+ })
+ }, checkInterval)
+ })
+}
diff --git a/src/utils/offchainState.ts b/src/utils/offchainState.ts
index 353e19322..1ea6d189f 100644
--- a/src/utils/offchainState.ts
+++ b/src/utils/offchainState.ts
@@ -60,6 +60,7 @@ const exportedStateMap: ExportedStateMap = {
EmailDeliveryAttempt: true,
Token: true,
NextEntityId: true,
+ BlockchainAccount: true, // this is a special case, because it's being created both in the mappings and the create/change account resolvers.
OrionOffchainCursor: true,
Channel: ['isExcluded', 'videoViewsNum', 'followsNum', 'yppStatus', 'channelWeight'],
Video: ['isExcluded', 'viewsNum', 'orionLanguage'],
@@ -142,9 +143,7 @@ export function setCrtNotificationPreferences(
function migrateExportDataToV400(data: ExportedData): ExportedData {
data.Account?.values.forEach((account) => {
// account will find himself with all CRT notification pref. enabled by default
- account.notificationPreferences = setCrtNotificationPreferences(
- account.notificationPreferences as AccountNotificationPreferences
- )
+ account.notificationPreferences = setCrtNotificationPreferences(account.notificationPreferences)
})
data.Notification?.values.forEach((notification) => {
@@ -159,11 +158,26 @@ function migrateExportDataToV400(data: ExportedData): ExportedData {
return data
}
+// TODO: How to upgrade to v5.0.0?
+// 1. Backup old db
+// 2. Export state using old code build (i.e. `npm run offchain-state:export`)
+// 3. Create copy of export.json file
+// 4. Build new code (v5.0.0)
+// 5. import state (i.e. `npm run offchain-state:import`)
+function migrateExportDataToV500(data: ExportedData): ExportedData {
+ data.Account?.values.forEach((account) => {
+ account.joystreamAccountId = (account as any).joystreamAccount
+ })
+
+ return data
+}
+
export class OffchainState {
private logger = createLogger('offchainState')
private _isImported = false
private migrations: Migrations = {
+ '5.0.0': migrateExportDataToV500,
'4.0.0': migrateExportDataToV400,
'3.2.0': migrateExportDataToV320,
'3.0.0': migrateExportDataToV300,