From a936f44c6e532efc1f559986c352594237ce3691 Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Thu, 14 Nov 2024 16:05:43 +0100 Subject: [PATCH 01/68] feat(user): implement stale anonymous users cleanup cli command refs #464 --- apps/api/drizzle.config.ts | 2 + apps/api/drizzle/0006_skinny_stingray.sql | 8 + apps/api/drizzle/meta/0006_snapshot.json | 286 ++++++++++++++++++ apps/api/drizzle/meta/_journal.json | 7 + .../api/src/auth/services/auth.interceptor.ts | 22 +- apps/api/src/billing/config/env.config.ts | 2 +- .../user-wallet/user-wallet.schema.ts | 2 +- .../src/billing/providers/wallet.provider.ts | 2 + .../user-wallet/user-wallet.repository.ts | 9 +- .../managed-user-wallet.service.ts | 28 +- .../rpc-message.service.ts | 13 +- .../services/tx-signer/tx-signer.service.ts | 2 +- apps/api/src/console.ts | 12 + .../src/core/repositories/base.repository.ts | 27 +- .../core/services/config/config.service.ts | 21 ++ apps/api/src/user/config/env.config.ts | 5 + .../user/controllers/user/user.controller.ts | 8 +- .../user/model-schemas/user/user.schema.ts | 1 + .../user/repositories/user/user.repository.ts | 14 +- .../stale-anonymous-users-cleaner.service.ts | 54 ++++ .../user-config/user-config.service.ts | 11 + .../test/functional/anonymous-user.spec.ts | 7 +- .../stale-anonymous-users-cleanup.spec.ts | 85 ++++++ apps/api/test/functional/user-init.spec.ts | 4 +- .../src/allowance/allowance-http.service.ts | 20 +- 25 files changed, 617 insertions(+), 35 deletions(-) create mode 100644 apps/api/drizzle/0006_skinny_stingray.sql create mode 100644 apps/api/drizzle/meta/0006_snapshot.json create mode 100644 apps/api/src/core/services/config/config.service.ts create mode 100644 apps/api/src/user/config/env.config.ts create mode 100644 apps/api/src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service.ts create mode 100644 apps/api/src/user/services/user-config/user-config.service.ts create mode 100644 apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts diff --git a/apps/api/drizzle.config.ts b/apps/api/drizzle.config.ts index 9ff7e3c66..135f1d065 100644 --- a/apps/api/drizzle.config.ts +++ b/apps/api/drizzle.config.ts @@ -1,3 +1,5 @@ +import "@akashnetwork/env-loader"; + import { defineConfig } from "drizzle-kit"; import { config } from "./src/core/config"; diff --git a/apps/api/drizzle/0006_skinny_stingray.sql b/apps/api/drizzle/0006_skinny_stingray.sql new file mode 100644 index 000000000..bba722a7b --- /dev/null +++ b/apps/api/drizzle/0006_skinny_stingray.sql @@ -0,0 +1,8 @@ +ALTER TABLE "user_wallets" DROP CONSTRAINT "user_wallets_user_id_userSetting_id_fk"; +--> statement-breakpoint +ALTER TABLE "userSetting" ADD COLUMN "last_active_at" timestamp DEFAULT now();--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "user_wallets" ADD CONSTRAINT "user_wallets_user_id_userSetting_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."userSetting"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/apps/api/drizzle/meta/0006_snapshot.json b/apps/api/drizzle/meta/0006_snapshot.json new file mode 100644 index 000000000..f1925992b --- /dev/null +++ b/apps/api/drizzle/meta/0006_snapshot.json @@ -0,0 +1,286 @@ +{ + "id": "d83b4940-34c1-400c-98d9-5a1eb935fe5e", + "prevId": "d6102ad7-0e0c-4ef8-8712-6a626f5ad2a1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.user_wallets": { + "name": "user_wallets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "deployment_allowance": { + "name": "deployment_allowance", + "type": "numeric(20, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0.00'" + }, + "fee_allowance": { + "name": "fee_allowance", + "type": "numeric(20, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0.00'" + }, + "trial": { + "name": "trial", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_wallets_user_id_userSetting_id_fk": { + "name": "user_wallets_user_id_userSetting_id_fk", + "tableFrom": "user_wallets", + "tableTo": "userSetting", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_wallets_user_id_unique": { + "name": "user_wallets_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "user_wallets_address_unique": { + "name": "user_wallets_address_unique", + "nullsNotDistinct": false, + "columns": [ + "address" + ] + } + } + }, + "public.checkout_sessions": { + "name": "checkout_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v4()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "checkout_sessions_user_id_userSetting_id_fk": { + "name": "checkout_sessions_user_id_userSetting_id_fk", + "tableFrom": "checkout_sessions", + "tableTo": "userSetting", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "checkout_sessions_session_id_unique": { + "name": "checkout_sessions_session_id_unique", + "nullsNotDistinct": false, + "columns": [ + "session_id" + ] + } + } + }, + "public.userSetting": { + "name": "userSetting", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v4()" + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscribedToNewsletter": { + "name": "subscribedToNewsletter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "youtubeUsername": { + "name": "youtubeUsername", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "twitterUsername": { + "name": "twitterUsername", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "githubUsername": { + "name": "githubUsername", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "userSetting_userId_unique": { + "name": "userSetting_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "userId" + ] + }, + "userSetting_username_unique": { + "name": "userSetting_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + } + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index f572b9904..da0149e47 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1724745930642, "tag": "0005_colorful_dreaming_celestial", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1731579448104, + "tag": "0006_skinny_stingray", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/auth/services/auth.interceptor.ts b/apps/api/src/auth/services/auth.interceptor.ts index 84e22dd3d..10a014880 100644 --- a/apps/api/src/auth/services/auth.interceptor.ts +++ b/apps/api/src/auth/services/auth.interceptor.ts @@ -6,7 +6,7 @@ import { AuthService } from "@src/auth/services/auth.service"; import { AuthTokenService } from "@src/auth/services/auth-token/auth-token.service"; import type { HonoInterceptor } from "@src/core/types/hono-interceptor.type"; import { kvStore } from "@src/middlewares/userMiddleware"; -import { UserRepository } from "@src/user/repositories"; +import { UserOutput, UserRepository } from "@src/user/repositories"; import { env } from "@src/utils/env"; import { getJwks, useKVStore, verify } from "@src/verify-rsa-jwt-cloudflare-worker-main"; @@ -27,10 +27,7 @@ export class AuthInterceptor implements HonoInterceptor { if (anonymousUserId) { const currentUser = await this.userRepository.findAnonymousById(anonymousUserId); - - this.authService.currentUser = currentUser; - this.authService.ability = currentUser ? this.abilityService.getAbilityFor("REGULAR_ANONYMOUS_USER", currentUser) : this.abilityService.EMPTY_ABILITY; - + await this.auth(currentUser); return await next(); } @@ -38,10 +35,7 @@ export class AuthInterceptor implements HonoInterceptor { if (userId) { const currentUser = await this.userRepository.findByUserId(userId); - - this.authService.currentUser = currentUser; - this.authService.ability = currentUser ? this.abilityService.getAbilityFor("REGULAR_USER", currentUser) : this.abilityService.EMPTY_ABILITY; - + this.auth(currentUser); return await next(); } @@ -51,6 +45,16 @@ export class AuthInterceptor implements HonoInterceptor { }; } + private async auth(user?: UserOutput) { + this.authService.currentUser = user; + if (user) { + this.authService.ability = this.abilityService.getAbilityFor(user.userId ? "REGULAR_ANONYMOUS_USER" : "REGULAR_USER", user); + await this.userRepository.markAsActive(user.id); + } else { + this.authService.ability = this.abilityService.EMPTY_ABILITY; + } + } + private async getValidUserId(bearer: string, c: Context) { const token = bearer.replace(/^Bearer\s+/i, ""); const jwks = await getJwks(env.AUTH0_JWKS_URI || c.env?.JWKS_URI, useKVStore(kvStore || c.env?.VERIFY_RSA_JWT), c.env?.VERIFY_RSA_JWT_JWKS_CACHE_KEY); diff --git a/apps/api/src/billing/config/env.config.ts b/apps/api/src/billing/config/env.config.ts index cf0d1366d..4030f2b2b 100644 --- a/apps/api/src/billing/config/env.config.ts +++ b/apps/api/src/billing/config/env.config.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -const envSchema = z.object({ +export const envSchema = z.object({ MASTER_WALLET_MNEMONIC: z.string(), UAKT_TOP_UP_MASTER_WALLET_MNEMONIC: z.string(), USDC_TOP_UP_MASTER_WALLET_MNEMONIC: z.string(), diff --git a/apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts b/apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts index 02084744d..476172f18 100644 --- a/apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts +++ b/apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts @@ -5,7 +5,7 @@ import { Users } from "@src/user/model-schemas"; export const UserWallets = pgTable("user_wallets", { id: serial("id").primaryKey(), userId: uuid("user_id") - .references(() => Users.id) + .references(() => Users.id, { onDelete: "cascade" }) .unique(), address: varchar("address").unique(), stripeCustomerId: varchar("stripe_customer_id"), diff --git a/apps/api/src/billing/providers/wallet.provider.ts b/apps/api/src/billing/providers/wallet.provider.ts index aec733c49..a62811805 100644 --- a/apps/api/src/billing/providers/wallet.provider.ts +++ b/apps/api/src/billing/providers/wallet.provider.ts @@ -14,3 +14,5 @@ export const USDC_TOP_UP_MASTER_WALLET = "USDC_TOP_UP_MASTER_WALLET"; container.register(USDC_TOP_UP_MASTER_WALLET, { useFactory: () => new MasterWalletService(config.USDC_TOP_UP_MASTER_WALLET_MNEMONIC) }); export const InjectWallet = (walletType: MasterWalletType) => inject(`${walletType}_MASTER_WALLET`); + +export const resolveWallet = (walletType: MasterWalletType) => container.resolve(`${walletType}_MASTER_WALLET`); diff --git a/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts b/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts index 24af4efcc..b3363d480 100644 --- a/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts +++ b/apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts @@ -1,4 +1,4 @@ -import { eq, lte } from "drizzle-orm"; +import { eq, inArray, lte } from "drizzle-orm"; import first from "lodash/first"; import omit from "lodash/omit"; import pick from "lodash/pick"; @@ -64,10 +64,15 @@ export class UserWalletRepository extends BaseRepository) { - const feeAllowances = await this.allowanceHttpService.getFeeAllowancesForGrantee(options.grantee); - const feeAllowance = feeAllowances.find(allowance => allowance.granter === options.granter); const results: Promise[] = []; - if (feeAllowance) { + if (await this.allowanceHttpService.hasFeeAllowance(options.granter, options.grantee)) { results.push(this.masterSigningClientService.executeTx([this.rpcMessageService.getRevokeAllowanceMsg(options)])); } @@ -111,4 +109,26 @@ export class ManagedUserWalletService { const deploymentAllowanceMsg = this.rpcMessageService.getDepositDeploymentGrantMsg(options); return await this.masterSigningClientService.executeTx([deploymentAllowanceMsg]); } + + async revokeAll(grantee: string, reason?: string) { + const masterWalletAddress = await this.masterWalletService.getFirstAddress(); + const params = { granter: masterWalletAddress, grantee }; + const messages: EncodeObject[] = []; + const revokeTypes: string[] = []; + + if (await this.allowanceHttpService.hasFeeAllowance(params.granter, params.grantee)) { + revokeTypes.push("REVOKE_ALLOWANCE"); + messages.push(this.rpcMessageService.getRevokeAllowanceMsg(params)); + } + + if (await this.allowanceHttpService.hasDeploymentGrant(params.granter, params.grantee)) { + revokeTypes.push("REVOKE_DEPOSIT_DEPLOYMENT_GRANT"); + messages.push(this.rpcMessageService.getRevokeDepositDeploymentGrantMsg(params)); + } + + if (messages.length) { + await this.masterSigningClientService.executeTx(messages); + this.logger.info({ event: "SPENDING_REVOKED", address: params.grantee, revokeTypes, reason }); + } + } } diff --git a/apps/api/src/billing/services/rpc-message-service/rpc-message.service.ts b/apps/api/src/billing/services/rpc-message-service/rpc-message.service.ts index 5af13c19d..d16baa45a 100644 --- a/apps/api/src/billing/services/rpc-message-service/rpc-message.service.ts +++ b/apps/api/src/billing/services/rpc-message-service/rpc-message.service.ts @@ -68,7 +68,7 @@ export class RpcMessageService { grantee, grant: { authorization: { - typeUrl: "/akash.deployment.v1beta3.DepositDeploymentAuthorization", + typeUrl: `/${DepositDeploymentAuthorization.$type}`, value: DepositDeploymentAuthorization.encode( DepositDeploymentAuthorization.fromPartial({ spendLimit: { @@ -100,6 +100,17 @@ export class RpcMessageService { }; } + getRevokeDepositDeploymentGrantMsg({ granter, grantee }: { granter: string; grantee: string }) { + return { + typeUrl: MsgRevoke.typeUrl, + value: MsgRevoke.fromPartial({ + granter: granter, + grantee: grantee, + msgTypeUrl: "/akash.deployment.v1beta3.MsgDepositDeployment" + }) + }; + } + getCloseDeploymentMsg(address: string, dseq: number) { return { typeUrl: `/${MsgCloseDeployment.$type}`, diff --git a/apps/api/src/billing/services/tx-signer/tx-signer.service.ts b/apps/api/src/billing/services/tx-signer/tx-signer.service.ts index dcf8f889c..d8db89ad9 100644 --- a/apps/api/src/billing/services/tx-signer/tx-signer.service.ts +++ b/apps/api/src/billing/services/tx-signer/tx-signer.service.ts @@ -39,7 +39,7 @@ export class TxSignerService { ) {} async signAndBroadcast(userId: UserWalletOutput["userId"], messages: StringifiedEncodeObject[]) { - const userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "sign").findByUserId(userId); + const userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "sign").findOneByUserId(userId); assert(userWallet, 404, "UserWallet Not Found"); const decodedMessages = this.decodeMessages(messages); diff --git a/apps/api/src/console.ts b/apps/api/src/console.ts index 41b8900bf..409f9b4ee 100644 --- a/apps/api/src/console.ts +++ b/apps/api/src/console.ts @@ -11,6 +11,8 @@ import { container } from "tsyringe"; import { WalletController } from "@src/billing/controllers/wallet/wallet.controller"; import { chainDb } from "@src/db/dbConnection"; import { TopUpDeploymentsController } from "@src/deployment/controllers/deployment/deployment.controller"; +import { UserController } from "@src/user/controllers/user/user.controller"; +import { UserConfigService } from "@src/user/services/user-config/user-config.service"; const program = new Command(); @@ -45,6 +47,16 @@ program }); }); +const userConfig = container.resolve(UserConfigService); +program + .command("cleanup-stale-anonymous-users") + .description(`Remove users that have been inactive for ${userConfig.get("STALE_ANONYMOUS_USERS_LIVE_IN_DAYS")} days`) + .action(async (options, command) => { + await executeCliHandler(command.name(), async () => { + await container.resolve(UserController).cleanUpStaleAnonymousUsers(); + }); + }); + const logger = LoggerService.forContext("CLI"); async function executeCliHandler(name: string, handler: () => Promise) { diff --git a/apps/api/src/core/repositories/base.repository.ts b/apps/api/src/core/repositories/base.repository.ts index 0fa9ddc9e..8ef0e18b8 100644 --- a/apps/api/src/core/repositories/base.repository.ts +++ b/apps/api/src/core/repositories/base.repository.ts @@ -1,5 +1,5 @@ import { AnyAbility } from "@casl/ability"; -import { and, DBQueryConfig, eq } from "drizzle-orm"; +import { and, DBQueryConfig, eq, inArray, sql } from "drizzle-orm"; import { PgTableWithColumns } from "drizzle-orm/pg-core/table"; import { SQL } from "drizzle-orm/sql/sql"; import first from "lodash/first"; @@ -98,15 +98,19 @@ export abstract class BaseRepository< return this.toOutputList(await this.queryCursor.findMany(params)); } - async paginate(options: { select?: Array; limit?: number; query?: Partial }, cb: (page: Output[]) => Promise) { + async paginate({ query, ...options }: { select?: Array; limit?: number; query?: Partial }, cb: (page: Output[]) => Promise) { + return this.paginateRaw({ ...options, where: this.queryToWhere(query) }, cb); + } + + protected async paginateRaw(params: Omit, "offset">, cb: (page: Output[]) => Promise) { let offset = 0; let hasNextPage = true; - const limit = options?.limit || 100; + params.limit = params.limit || 100; while (hasNextPage) { - const items = await this.find(options.query, { select: options.select, offset, limit }); + const items = this.toOutputList(await this.queryCursor.findMany({ ...params, offset })); offset += items.length; - hasNextPage = items.length === limit; + hasNextPage = items.length === params.limit; if (items.length) { await cb(items); @@ -123,7 +127,13 @@ export abstract class BaseRepository< async updateBy(query: Partial, payload: Partial, options?: MutationOptions): Promise; async updateBy(query: Partial, payload: Partial): Promise; async updateBy(query: Partial, payload: Partial, options?: MutationOptions): Promise { - const cursor = this.cursor.update(this.table).set(this.toInput(payload)).where(this.queryToWhere(query)); + const cursor = this.cursor + .update(this.table) + .set({ + ...this.toInput(payload), + updated_at: sql`now()` + }) + .where(this.queryToWhere(query)); if (options?.returning) { const items = await cursor.returning(); @@ -135,6 +145,11 @@ export abstract class BaseRepository< return undefined; } + async deleteById(id: Output["id"] | Output["id"][]): Promise { + const where = Array.isArray(id) ? inArray(this.table.id, id) : eq(this.table.id, id); + await this.cursor.delete(this.table).where(this.whereAccessibleBy(where)); + } + async deleteBy(query: Partial, options?: MutationOptions): Promise; async deleteBy(query: Partial): Promise; async deleteBy(query: Partial, options?: MutationOptions): Promise { diff --git a/apps/api/src/core/services/config/config.service.ts b/apps/api/src/core/services/config/config.service.ts new file mode 100644 index 000000000..6cdeae820 --- /dev/null +++ b/apps/api/src/core/services/config/config.service.ts @@ -0,0 +1,21 @@ +import { z, ZodObject, ZodRawShape } from "zod"; + +interface ConfigServiceOptions, C extends Record> { + envSchema?: E; + config?: C; +} + +export class ConfigService, C extends Record> { + private readonly config: C & z.infer; + + constructor(options: ConfigServiceOptions) { + this.config = { + ...options.config, + ...options.envSchema?.parse(process.env) + }; + } + + get(key: K): (typeof this.config)[K] { + return this.config[key]; + } +} diff --git a/apps/api/src/user/config/env.config.ts b/apps/api/src/user/config/env.config.ts new file mode 100644 index 000000000..258634d18 --- /dev/null +++ b/apps/api/src/user/config/env.config.ts @@ -0,0 +1,5 @@ +import { z } from "zod"; + +export const envSchema = z.object({ + STALE_ANONYMOUS_USERS_LIVE_IN_DAYS: z.number().optional().default(90) +}); diff --git a/apps/api/src/user/controllers/user/user.controller.ts b/apps/api/src/user/controllers/user/user.controller.ts index 3d74e06f5..5d02fe406 100644 --- a/apps/api/src/user/controllers/user/user.controller.ts +++ b/apps/api/src/user/controllers/user/user.controller.ts @@ -6,13 +6,15 @@ import { AuthTokenService } from "@src/auth/services/auth-token/auth-token.servi import { UserRepository } from "@src/user/repositories"; import { GetUserParams } from "@src/user/routes/get-anonymous-user/get-anonymous-user.router"; import { AnonymousUserResponseOutput } from "@src/user/schemas/user.schema"; +import { StaleAnonymousUsersCleanerService } from "@src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service"; @singleton() export class UserController { constructor( private readonly userRepository: UserRepository, private readonly authService: AuthService, - private readonly anonymousUserAuthService: AuthTokenService + private readonly anonymousUserAuthService: AuthTokenService, + private readonly staleAnonymousUsersCleanerService: StaleAnonymousUsersCleanerService ) {} async create(): Promise { @@ -31,4 +33,8 @@ export class UserController { return { data: user }; } + + async cleanUpStaleAnonymousUsers() { + await this.staleAnonymousUsersCleanerService.cleanUpStaleAnonymousUsers(); + } } diff --git a/apps/api/src/user/model-schemas/user/user.schema.ts b/apps/api/src/user/model-schemas/user/user.schema.ts index 70bf89437..7b16c1881 100644 --- a/apps/api/src/user/model-schemas/user/user.schema.ts +++ b/apps/api/src/user/model-schemas/user/user.schema.ts @@ -16,5 +16,6 @@ export const Users = pgTable("userSetting", { youtubeUsername: varchar("youtubeUsername", { length: 255 }), twitterUsername: varchar("twitterUsername", { length: 255 }), githubUsername: varchar("githubUsername", { length: 255 }), + lastActiveAt: timestamp("last_active_at").defaultNow(), createdAt: timestamp("created_at").defaultNow() }); diff --git a/apps/api/src/user/repositories/user/user.repository.ts b/apps/api/src/user/repositories/user/user.repository.ts index 8f75d64af..d2bcbb632 100644 --- a/apps/api/src/user/repositories/user/user.repository.ts +++ b/apps/api/src/user/repositories/user/user.repository.ts @@ -1,4 +1,5 @@ -import { and, eq, isNull } from "drizzle-orm"; +import subDays from "date-fns/subDays"; +import { and, eq, isNull, lte, sql } from "drizzle-orm"; import first from "lodash/first"; import { singleton } from "tsyringe"; @@ -34,4 +35,15 @@ export class UserRepository extends BaseRepository Promise) { + await this.paginateRaw({ where: and(isNull(this.table.userId), lte(this.table.lastActiveAt, subDays(new Date(), inactivityInDays))), ...params }, cb); + } } diff --git a/apps/api/src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service.ts b/apps/api/src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service.ts new file mode 100644 index 000000000..a714bef4f --- /dev/null +++ b/apps/api/src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service.ts @@ -0,0 +1,54 @@ +import { LoggerService } from "@akashnetwork/logging"; +import { PromisePool } from "@supercharge/promise-pool"; +import difference from "lodash/difference"; +import { singleton } from "tsyringe"; + +import { UserWalletRepository } from "@src/billing/repositories"; +import { ManagedUserWalletService } from "@src/billing/services"; +import { InjectSentry, Sentry } from "@src/core/providers/sentry.provider"; +import { SentryEventService } from "@src/core/services/sentry-event/sentry-event.service"; +import { UserRepository } from "@src/user/repositories"; +import { UserConfigService } from "@src/user/services/user-config/user-config.service"; + +@singleton() +export class StaleAnonymousUsersCleanerService { + private readonly CONCURRENCY = 10; + + private readonly logger = LoggerService.forContext(StaleAnonymousUsersCleanerService.name); + + constructor( + private readonly userRepository: UserRepository, + private readonly userWalletRepository: UserWalletRepository, + private readonly managedUserWalletService: ManagedUserWalletService, + private readonly config: UserConfigService, + @InjectSentry() private readonly sentry: Sentry, + private readonly sentryEventService: SentryEventService + ) {} + + async cleanUpStaleAnonymousUsers() { + await this.userRepository.paginateStaleAnonymousUsers( + { inactivityInDays: this.config.get("STALE_ANONYMOUS_USERS_LIVE_IN_DAYS"), limit: this.CONCURRENCY }, + async users => { + const userIds = users.map(user => user.id); + const wallets = await this.userWalletRepository.findByUserId(users.map(user => user.id)); + const { errors } = await PromisePool.withConcurrency(this.CONCURRENCY) + .for(wallets) + .process(async wallet => { + await this.managedUserWalletService.revokeAll(wallet.address, "USER_INACTIVITY"); + }); + const erroredUserIds = errors.map(({ item }) => item.userId); + const userIdsToRemove = difference(userIds, erroredUserIds); + + if (userIdsToRemove.length) { + await this.userRepository.deleteById(userIdsToRemove); + this.logger.debug({ event: "STALE_ANONYMOUS_USERS_CLEANUP", userIds: userIdsToRemove }); + } + + if (errors.length) { + this.logger.debug({ event: "STALE_ANONYMOUS_USERS_REVOKE_ERROR", errors }); + this.sentry.captureEvent(this.sentryEventService.toEvent(errors)); + } + } + ); + } +} diff --git a/apps/api/src/user/services/user-config/user-config.service.ts b/apps/api/src/user/services/user-config/user-config.service.ts new file mode 100644 index 000000000..482367d6d --- /dev/null +++ b/apps/api/src/user/services/user-config/user-config.service.ts @@ -0,0 +1,11 @@ +import { singleton } from "tsyringe"; + +import { ConfigService } from "@src/core/services/config/config.service"; +import { envSchema } from "@src/user/config/env.config"; + +@singleton() +export class UserConfigService extends ConfigService { + constructor() { + super({ envSchema }); + } +} diff --git a/apps/api/test/functional/anonymous-user.spec.ts b/apps/api/test/functional/anonymous-user.spec.ts index d7acdc680..073a11d01 100644 --- a/apps/api/test/functional/anonymous-user.spec.ts +++ b/apps/api/test/functional/anonymous-user.spec.ts @@ -38,7 +38,12 @@ describe("Users", () => { }); const retrievedUser = await getUserResponse.json(); - expect(retrievedUser).toMatchObject({ data: user }); + expect(retrievedUser).toMatchObject({ + data: { + ...user, + lastActiveAt: expect.any(String) + } + }); }); it("should throw 401 provided no auth header", async () => { diff --git a/apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts b/apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts new file mode 100644 index 000000000..10f867419 --- /dev/null +++ b/apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts @@ -0,0 +1,85 @@ +import { AllowanceHttpService } from "@akashnetwork/http-sdk"; +import subDays from "date-fns/subDays"; +import { container } from "tsyringe"; + +import { app } from "@src/app"; +import { resolveWallet } from "@src/billing/providers/wallet.provider"; +import { UserWalletRepository } from "@src/billing/repositories"; +import { UserController } from "@src/user/controllers/user/user.controller"; +import { UserRepository } from "@src/user/repositories"; + +import { DbTestingService } from "@test/services/db-testing.service"; +import { WalletTestingService } from "@test/services/wallet-testing.service"; + +jest.setTimeout(50000); + +describe("Users", () => { + const dbService = container.resolve(DbTestingService); + const userRepository = container.resolve(UserRepository); + const userWalletRepository = container.resolve(UserWalletRepository); + const walletService = new WalletTestingService(app); + const controller = container.resolve(UserController); + const allowanceHttpService = container.resolve(AllowanceHttpService); + const masterWalletService = resolveWallet("MANAGED"); + let masterAddress: string; + + beforeAll(async () => { + masterAddress = await masterWalletService.getFirstAddress(); + }); + + afterEach(async () => { + await dbService.cleanAll(); + }); + + describe("stale anonymous users cleanup", () => { + it("should remove anonymous users inactive for defined period", async () => { + const [stale, reactivated, recent, invalidAddress, staleNoWallet, recentNoWallet] = await Promise.all([ + walletService.createUserAndWallet(), + walletService.createUserAndWallet(), + walletService.createUserAndWallet(), + walletService.createUserAndWallet(), + walletService.createUser(), + walletService.createUser() + ]); + + const staleParams = { lastActiveAt: subDays(new Date(), 91) }; + await Promise.all([ + userRepository.updateById(stale.user.id, staleParams), + userRepository.updateById(staleNoWallet.user.id, staleParams), + userRepository.updateById(reactivated.user.id, staleParams), + userRepository.updateById(invalidAddress.user.id, staleParams), + userWalletRepository.updateById(invalidAddress.wallet.id, { address: "invalid" }) + ]); + + const reactivate = walletService.getWalletByUserId(reactivated.user.id, reactivated.token); + await reactivate; + + await controller.cleanUpStaleAnonymousUsers(); + + const [users, wallets] = await Promise.all([userRepository.find(), userWalletRepository.find()]); + + expect(users).toHaveLength(4); + expect(wallets).toHaveLength(3); + + expect(users).toMatchObject( + expect.arrayContaining([ + expect.objectContaining({ id: recent.user.id }), + expect.objectContaining({ id: reactivated.user.id }), + expect.objectContaining({ id: recentNoWallet.user.id }), + expect.objectContaining({ id: invalidAddress.user.id }) + ]) + ); + + await Promise.all([ + expect(allowanceHttpService.hasFeeAllowance(recent.wallet.address, masterAddress)).resolves.toBeFalsy(), + expect(allowanceHttpService.hasDeploymentGrant(recent.wallet.address, masterAddress)).resolves.toBeFalsy(), + + expect(allowanceHttpService.hasFeeAllowance(reactivated.wallet.address, masterAddress)).resolves.toBeFalsy(), + expect(allowanceHttpService.hasDeploymentGrant(reactivated.wallet.address, masterAddress)).resolves.toBeFalsy(), + + expect(allowanceHttpService.hasFeeAllowance(stale.wallet.address, masterAddress)).resolves.toBeFalsy(), + expect(allowanceHttpService.hasDeploymentGrant(stale.wallet.address, masterAddress)).resolves.toBeFalsy() + ]); + }); + }); +}); diff --git a/apps/api/test/functional/user-init.spec.ts b/apps/api/test/functional/user-init.spec.ts index 42d3ddc3b..7c2b5a9dd 100644 --- a/apps/api/test/functional/user-init.spec.ts +++ b/apps/api/test/functional/user-init.spec.ts @@ -77,7 +77,7 @@ describe("User Init", () => { const res = await sendTokenInfo(); expect(res.status).toBe(200); - expect(res.body).toMatchObject(omit(existingUser, "createdAt")); + expect(res.body).toMatchObject(omit(existingUser, ["createdAt", "lastActiveAt"])); }); it("should register an anonymous user", async () => { @@ -86,7 +86,7 @@ describe("User Init", () => { expect(res.status).toBe(200); expect(res.body).toMatchObject({ - ...omit(anonymousUser, ["createdAt", "username"]), + ...omit(anonymousUser, ["createdAt", "lastActiveAt", "username"]), ...omit(auth0Payload, "wantedUsername") }); }); diff --git a/packages/http-sdk/src/allowance/allowance-http.service.ts b/packages/http-sdk/src/allowance/allowance-http.service.ts index b1a3a2deb..402ba3d05 100644 --- a/packages/http-sdk/src/allowance/allowance-http.service.ts +++ b/packages/http-sdk/src/allowance/allowance-http.service.ts @@ -1,4 +1,4 @@ -import type { AxiosRequestConfig } from "axios"; +import { AxiosRequestConfig } from "axios"; import { HttpService } from "../http/http.service"; import type { Denom } from "../types/denom.type"; @@ -50,17 +50,27 @@ export class AllowanceHttpService extends HttpService { async getFeeAllowancesForGrantee(address: string) { const allowances = this.extractData(await this.get(`cosmos/feegrant/v1beta1/allowances/${address}`)); - return allowances.allowances; + return allowances.allowances.filter(allowance => allowance.allowance["@type"] === "/cosmos.feegrant.v1beta1.BasicAllowance"); } async getFeeAllowanceForGranterAndGrantee(granter: string, grantee: string) { const allowances = this.extractData(await this.get(`cosmos/feegrant/v1beta1/allowance/${granter}/${grantee}`)); - return allowances.allowance; + return allowances.allowance.allowance["@type"] === "/cosmos.feegrant.v1beta1.BasicAllowance" ? allowances.allowance : undefined; } async getDeploymentAllowancesForGrantee(address: string) { const allowances = this.extractData(await this.get(`cosmos/authz/v1beta1/grants/grantee/${address}`)); - return allowances.grants; + return allowances.grants.filter(grant => grant.authorization["@type"] === "/akash.deployment.v1beta3.DepositDeploymentAuthorization"); + } + + async hasFeeAllowance(granter: string, grantee: string) { + const feeAllowances = await this.getFeeAllowancesForGrantee(grantee); + return feeAllowances.some(allowance => allowance.granter === granter); + } + + async hasDeploymentGrant(granter: string, grantee: string) { + const feeAllowances = await this.getDeploymentAllowancesForGrantee(grantee); + return feeAllowances.some(allowance => allowance.granter === granter); } async paginateDeploymentGrants( @@ -84,7 +94,7 @@ export class AllowanceHttpService extends HttpService { ); nextPageKey = response.pagination.next_key; - await cb(response.grants); + await cb(response.grants.filter(grant => grant.authorization["@type"] === "/akash.deployment.v1beta3.DepositDeploymentAuthorization")); } while (nextPageKey); } } From 10d03a683f416a6c0f685614705626339268ba1b Mon Sep 17 00:00:00 2001 From: CI Date: Fri, 15 Nov 2024 08:23:57 +0000 Subject: [PATCH 02/68] chore(release): released version console-api/v2.30.0-beta.0 --- apps/api/CHANGELOG.md | 7 +++++++ apps/api/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/api/CHANGELOG.md b/apps/api/CHANGELOG.md index e915c438d..3dc11f674 100644 --- a/apps/api/CHANGELOG.md +++ b/apps/api/CHANGELOG.md @@ -1,5 +1,12 @@ +## [2.30.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.29.1...console-api/v2.30.0-beta.0) (2024-11-15) + + +### Features + +* **user:** implement stale anonymous users cleanup cli command ([a936f44](https://github.com/akash-network/console/commit/a936f44c6e532efc1f559986c352594237ce3691)), closes [#464](https://github.com/akash-network/console/issues/464) + ## [2.29.1](https://github.com/akash-network/console/compare/console-api/v2.29.1-beta.0...console-api/v2.29.1) (2024-11-13) ## [2.29.1-beta.0](https://github.com/akash-network/console/compare/console-api/v2.29.0...console-api/v2.29.1-beta.0) (2024-11-13) diff --git a/apps/api/package.json b/apps/api/package.json index d51bbdccd..26c48ff45 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-api", - "version": "2.29.1", + "version": "2.30.0-beta.0", "description": "Api providing data to the deploy tool", "repository": { "type": "git", From 3fc959eb1f7ac1132eab054909a6336263482db8 Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Fri, 15 Nov 2024 10:49:26 +0100 Subject: [PATCH 03/68] fix(observability): set logger time format to iso --- packages/logging/src/servicies/logger/logger.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/logging/src/servicies/logger/logger.service.ts b/packages/logging/src/servicies/logger/logger.service.ts index b856ea929..b1ccca278 100644 --- a/packages/logging/src/servicies/logger/logger.service.ts +++ b/packages/logging/src/servicies/logger/logger.service.ts @@ -31,6 +31,7 @@ export class LoggerService implements Logger { let options: LoggerOptions = { level: config.LOG_LEVEL, mixin: LoggerService.mixin, + timestamp: () => `,"time":"${new Date().toISOString()}"`, ...inputOptions }; From a29eee8b956defd9b23c985915976d6049247b66 Mon Sep 17 00:00:00 2001 From: CI Date: Fri, 15 Nov 2024 11:10:58 +0000 Subject: [PATCH 04/68] chore(release): released version console-api/v2.30.0 --- apps/api/CHANGELOG.md | 2 ++ apps/api/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/api/CHANGELOG.md b/apps/api/CHANGELOG.md index 3dc11f674..97d5f3c71 100644 --- a/apps/api/CHANGELOG.md +++ b/apps/api/CHANGELOG.md @@ -1,5 +1,7 @@ +## [2.30.0](https://github.com/akash-network/console/compare/console-api/v2.30.0-beta.0...console-api/v2.30.0) (2024-11-15) + ## [2.30.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.29.1...console-api/v2.30.0-beta.0) (2024-11-15) diff --git a/apps/api/package.json b/apps/api/package.json index 26c48ff45..d0ddfeb73 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-api", - "version": "2.30.0-beta.0", + "version": "2.30.0", "description": "Api providing data to the deploy tool", "repository": { "type": "git", From b258c6389d22c0bf57e9c702b51a1280faf74eb7 Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Fri, 15 Nov 2024 12:18:43 +0100 Subject: [PATCH 05/68] fix(observability): bump logger version --- apps/api/mvm.lock | 2 +- packages/logging/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/mvm.lock b/apps/api/mvm.lock index 7e5d99ebf..e3c3f96a8 100644 --- a/apps/api/mvm.lock +++ b/apps/api/mvm.lock @@ -3,6 +3,6 @@ "@akashnetwork/database": "1.0.0", "@akashnetwork/env-loader": "1.0.1", "@akashnetwork/http-sdk": "1.0.8", - "@akashnetwork/logging": "2.0.0" + "@akashnetwork/logging": "2.0.1" } } diff --git a/packages/logging/package.json b/packages/logging/package.json index 38cbe9f25..f5a39880b 100644 --- a/packages/logging/package.json +++ b/packages/logging/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/logging", - "version": "2.0.0", + "version": "2.0.1", "description": "Package containing logging tools", "main": "src/index.ts", "scripts": { From fbd3fcde53319cd171a50808daa212a606698e15 Mon Sep 17 00:00:00 2001 From: CI Date: Fri, 15 Nov 2024 11:23:53 +0000 Subject: [PATCH 06/68] chore(release): released version console-api/v2.30.1-beta.0 --- apps/api/CHANGELOG.md | 7 +++++++ apps/api/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/api/CHANGELOG.md b/apps/api/CHANGELOG.md index 97d5f3c71..453e1a255 100644 --- a/apps/api/CHANGELOG.md +++ b/apps/api/CHANGELOG.md @@ -1,5 +1,12 @@ +## [2.30.1-beta.0](https://github.com/akash-network/console/compare/console-api/v2.30.0...console-api/v2.30.1-beta.0) (2024-11-15) + + +### Bug Fixes + +* **observability:** bump logger version ([b258c63](https://github.com/akash-network/console/commit/b258c6389d22c0bf57e9c702b51a1280faf74eb7)) + ## [2.30.0](https://github.com/akash-network/console/compare/console-api/v2.30.0-beta.0...console-api/v2.30.0) (2024-11-15) ## [2.30.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.29.1...console-api/v2.30.0-beta.0) (2024-11-15) diff --git a/apps/api/package.json b/apps/api/package.json index d0ddfeb73..e3b83f133 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-api", - "version": "2.30.0", + "version": "2.30.1-beta.0", "description": "Api providing data to the deploy tool", "repository": { "type": "git", From 26b25a85030eefb97c6d7d4e12cad7b55b89a6dd Mon Sep 17 00:00:00 2001 From: CI Date: Fri, 15 Nov 2024 11:47:51 +0000 Subject: [PATCH 07/68] chore(release): released version console-api/v2.30.1 --- apps/api/CHANGELOG.md | 2 ++ apps/api/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/api/CHANGELOG.md b/apps/api/CHANGELOG.md index 453e1a255..0679bdcad 100644 --- a/apps/api/CHANGELOG.md +++ b/apps/api/CHANGELOG.md @@ -1,5 +1,7 @@ +## [2.30.1](https://github.com/akash-network/console/compare/console-api/v2.30.1-beta.0...console-api/v2.30.1) (2024-11-15) + ## [2.30.1-beta.0](https://github.com/akash-network/console/compare/console-api/v2.30.0...console-api/v2.30.1-beta.0) (2024-11-15) diff --git a/apps/api/package.json b/apps/api/package.json index e3b83f133..9498c4f58 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-api", - "version": "2.30.1-beta.0", + "version": "2.30.1", "description": "Api providing data to the deploy tool", "repository": { "type": "git", From 54cae5d0f3c37dd6fe6623bcc249379f99cad247 Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Mon, 18 Nov 2024 12:51:04 +0100 Subject: [PATCH 08/68] feat(deployment): implement concurrency option for stale deployments cleaner --- apps/api/src/console.ts | 4 +++- .../controllers/deployment/deployment.controller.ts | 9 ++++++--- .../stale-managed-deployments-cleaner.service.ts | 8 ++++++-- apps/deploy-web/next-env.d.ts | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/api/src/console.ts b/apps/api/src/console.ts index 409f9b4ee..a17d37ac0 100644 --- a/apps/api/src/console.ts +++ b/apps/api/src/console.ts @@ -7,6 +7,7 @@ import { LoggerService } from "@akashnetwork/logging"; import { context, trace } from "@opentelemetry/api"; import { Command } from "commander"; import { container } from "tsyringe"; +import { z } from "zod"; import { WalletController } from "@src/billing/controllers/wallet/wallet.controller"; import { chainDb } from "@src/db/dbConnection"; @@ -41,9 +42,10 @@ program program .command("cleanup-stale-deployments") .description("Close deployments without leases created at least 10min ago") + .option("-c, --concurrency ", "How much wallets is processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value)) .action(async (options, command) => { await executeCliHandler(command.name(), async () => { - await container.resolve(TopUpDeploymentsController).cleanUpStaleDeployment(); + await container.resolve(TopUpDeploymentsController).cleanUpStaleDeployment(options); }); }); diff --git a/apps/api/src/deployment/controllers/deployment/deployment.controller.ts b/apps/api/src/deployment/controllers/deployment/deployment.controller.ts index f63b1f09c..b6d8157cc 100644 --- a/apps/api/src/deployment/controllers/deployment/deployment.controller.ts +++ b/apps/api/src/deployment/controllers/deployment/deployment.controller.ts @@ -1,6 +1,9 @@ import { singleton } from "tsyringe"; -import { StaleManagedDeploymentsCleanerService } from "@src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service"; +import { + CleanUpStaleDeploymentsParams, + StaleManagedDeploymentsCleanerService +} from "@src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service"; import { TopUpCustodialDeploymentsService } from "@src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service"; import { TopUpManagedDeploymentsService } from "@src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service"; import { TopUpDeploymentsOptions } from "@src/deployment/types/deployments-refiller"; @@ -18,7 +21,7 @@ export class TopUpDeploymentsController { await this.topUpManagedDeploymentsService.topUpDeployments(options); } - async cleanUpStaleDeployment() { - await this.staleDeploymentsCleanerService.cleanup(); + async cleanUpStaleDeployment(options: CleanUpStaleDeploymentsParams) { + await this.staleDeploymentsCleanerService.cleanup(options); } } diff --git a/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts b/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts index 1fe067e01..8280d2318 100644 --- a/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts +++ b/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts @@ -11,6 +11,10 @@ import { ErrorService } from "@src/core/services/error/error.service"; import { DeploymentRepository } from "@src/deployment/repositories/deployment/deployment.repository"; import { averageBlockTime } from "@src/utils/constants"; +export interface CleanUpStaleDeploymentsParams { + concurrency: number; +} + @singleton() export class StaleManagedDeploymentsCleanerService { private readonly logger = LoggerService.forContext(StaleManagedDeploymentsCleanerService.name); @@ -28,8 +32,8 @@ export class StaleManagedDeploymentsCleanerService { private readonly errorService: ErrorService ) {} - async cleanup() { - await this.userWalletRepository.paginate({ limit: 10 }, async wallets => { + async cleanup(options: CleanUpStaleDeploymentsParams) { + await this.userWalletRepository.paginate({ limit: options.concurrency || 10 }, async wallets => { const cleanUpAllWallets = wallets.map(async wallet => { await this.errorService.execWithErrorHandler( { diff --git a/apps/deploy-web/next-env.d.ts b/apps/deploy-web/next-env.d.ts index 4f11a03dc..a4a7b3f5c 100644 --- a/apps/deploy-web/next-env.d.ts +++ b/apps/deploy-web/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. From 61752e90fecc559eade828c721fa54839d8aef49 Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Fri, 15 Nov 2024 13:45:44 +0100 Subject: [PATCH 09/68] feat(user): implement dry run and summary logging for stale anonymous users cleaner refs #464 --- .../managed-user-wallet.service.ts | 23 +++++++++++---- apps/api/src/console.ts | 5 ++-- apps/api/src/core/types/console.ts | 3 ++ .../top-up-managed-deployments.service.ts | 2 +- .../deployment/types/deployments-refiller.ts | 6 ++-- .../user/controllers/user/user.controller.ts | 9 ++++-- ...nymous-users-cleaner-summarizer.service.ts | 29 +++++++++++++++++++ .../stale-anonymous-users-cleaner.service.ts | 29 ++++++++++++++----- .../stale-anonymous-users-cleanup.spec.ts | 2 +- 9 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 apps/api/src/core/types/console.ts create mode 100644 apps/api/src/user/services/stale-anonymous-users-cleaner-summarizer/stale-anonymous-users-cleaner-summarizer.service.ts diff --git a/apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts b/apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts index 392ac085e..fce79862e 100644 --- a/apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts +++ b/apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts @@ -12,6 +12,7 @@ import { InjectWallet } from "@src/billing/providers/wallet.provider"; import { MasterSigningClientService } from "@src/billing/services/master-signing-client/master-signing-client.service"; import { MasterWalletService } from "@src/billing/services/master-wallet/master-wallet.service"; import { RpcMessageService, SpendingAuthorizationMsgOptions } from "@src/billing/services/rpc-message-service/rpc-message.service"; +import { DryRunOptions } from "@src/core/types/console"; interface SpendingAuthorizationOptions { address: string; @@ -110,25 +111,35 @@ export class ManagedUserWalletService { return await this.masterSigningClientService.executeTx([deploymentAllowanceMsg]); } - async revokeAll(grantee: string, reason?: string) { + async revokeAll(grantee: string, reason?: string, options?: DryRunOptions) { const masterWalletAddress = await this.masterWalletService.getFirstAddress(); const params = { granter: masterWalletAddress, grantee }; const messages: EncodeObject[] = []; - const revokeTypes: string[] = []; + const revokeSummary = { + feeAllowance: false, + deploymentGrant: false + }; if (await this.allowanceHttpService.hasFeeAllowance(params.granter, params.grantee)) { - revokeTypes.push("REVOKE_ALLOWANCE"); + revokeSummary.feeAllowance = true; messages.push(this.rpcMessageService.getRevokeAllowanceMsg(params)); } if (await this.allowanceHttpService.hasDeploymentGrant(params.granter, params.grantee)) { - revokeTypes.push("REVOKE_DEPOSIT_DEPLOYMENT_GRANT"); + revokeSummary.deploymentGrant = true; messages.push(this.rpcMessageService.getRevokeDepositDeploymentGrantMsg(params)); } - if (messages.length) { + if (!messages.length) { + return; + } + + if (!options?.dryRun) { await this.masterSigningClientService.executeTx(messages); - this.logger.info({ event: "SPENDING_REVOKED", address: params.grantee, revokeTypes, reason }); } + + this.logger.info({ event: "SPENDING_REVOKED", address: params.grantee, revokeSummary, reason }); + + return revokeSummary; } } diff --git a/apps/api/src/console.ts b/apps/api/src/console.ts index a17d37ac0..e2ec98ff6 100644 --- a/apps/api/src/console.ts +++ b/apps/api/src/console.ts @@ -31,8 +31,8 @@ program program .command("top-up-deployments") - .option("-d, --dry-run", "Dry run the top up deployments", false) .description("Refill deployments with auto top up enabled") + .option("-d, --dry-run", "Dry run the top up deployments", false) .action(async (options, command) => { await executeCliHandler(command.name(), async () => { await container.resolve(TopUpDeploymentsController).topUpDeployments({ dryRun: options.dryRun }); @@ -53,9 +53,10 @@ const userConfig = container.resolve(UserConfigService); program .command("cleanup-stale-anonymous-users") .description(`Remove users that have been inactive for ${userConfig.get("STALE_ANONYMOUS_USERS_LIVE_IN_DAYS")} days`) + .option("-d, --dry-run", "Dry run the clean up stale anonymous users", false) .action(async (options, command) => { await executeCliHandler(command.name(), async () => { - await container.resolve(UserController).cleanUpStaleAnonymousUsers(); + await container.resolve(UserController).cleanUpStaleAnonymousUsers({ dryRun: options.dryRun }); }); }); diff --git a/apps/api/src/core/types/console.ts b/apps/api/src/core/types/console.ts new file mode 100644 index 000000000..a53d4170a --- /dev/null +++ b/apps/api/src/core/types/console.ts @@ -0,0 +1,3 @@ +export interface DryRunOptions { + dryRun: boolean; +} diff --git a/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts b/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts index 38428f58c..bd8572510 100644 --- a/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts +++ b/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts @@ -48,7 +48,7 @@ export class TopUpManagedDeploymentsService implements DeploymentsRefiller { }); summary.set("endBlockHeight", await this.blockHttpService.getCurrentHeight()); - this.logger.info({ event: "TOP_UP_SUMMARY", summary: summary.summarize() }); + this.logger.info({ event: "TOP_UP_SUMMARY", summary: summary.summarize(), dryRun: options.dryRun }); } private async topUpForWallet(wallet: UserWalletOutput, options: TopUpDeploymentsOptions, summary: TopUpSummarizer) { diff --git a/apps/api/src/deployment/types/deployments-refiller.ts b/apps/api/src/deployment/types/deployments-refiller.ts index cddc764dd..c49df7631 100644 --- a/apps/api/src/deployment/types/deployments-refiller.ts +++ b/apps/api/src/deployment/types/deployments-refiller.ts @@ -1,6 +1,6 @@ -export interface TopUpDeploymentsOptions { - dryRun: boolean; -} +import { DryRunOptions } from "@src/core/types/console"; + +export interface TopUpDeploymentsOptions extends DryRunOptions {} export interface DeploymentsRefiller { topUpDeployments(options: TopUpDeploymentsOptions): Promise; diff --git a/apps/api/src/user/controllers/user/user.controller.ts b/apps/api/src/user/controllers/user/user.controller.ts index 5d02fe406..42b1b7ec2 100644 --- a/apps/api/src/user/controllers/user/user.controller.ts +++ b/apps/api/src/user/controllers/user/user.controller.ts @@ -6,7 +6,10 @@ import { AuthTokenService } from "@src/auth/services/auth-token/auth-token.servi import { UserRepository } from "@src/user/repositories"; import { GetUserParams } from "@src/user/routes/get-anonymous-user/get-anonymous-user.router"; import { AnonymousUserResponseOutput } from "@src/user/schemas/user.schema"; -import { StaleAnonymousUsersCleanerService } from "@src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service"; +import { + StaleAnonymousUsersCleanerOptions, + StaleAnonymousUsersCleanerService +} from "@src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service"; @singleton() export class UserController { @@ -34,7 +37,7 @@ export class UserController { return { data: user }; } - async cleanUpStaleAnonymousUsers() { - await this.staleAnonymousUsersCleanerService.cleanUpStaleAnonymousUsers(); + async cleanUpStaleAnonymousUsers(options: StaleAnonymousUsersCleanerOptions) { + await this.staleAnonymousUsersCleanerService.cleanUpStaleAnonymousUsers(options); } } diff --git a/apps/api/src/user/services/stale-anonymous-users-cleaner-summarizer/stale-anonymous-users-cleaner-summarizer.service.ts b/apps/api/src/user/services/stale-anonymous-users-cleaner-summarizer/stale-anonymous-users-cleaner-summarizer.service.ts new file mode 100644 index 000000000..5d26012c1 --- /dev/null +++ b/apps/api/src/user/services/stale-anonymous-users-cleaner-summarizer/stale-anonymous-users-cleaner-summarizer.service.ts @@ -0,0 +1,29 @@ +interface StaleAnonymousUsersCleanerSummary { + feeAllowanceRevokeCount: number; + deploymentGrantRevokeCount: number; + revokeErrorCount: number; + usersDroppedCount: number; +} + +export class StaleAnonymousUsersCleanerSummarizer { + private feeAllowanceRevokeCount = 0; + + private deploymentGrantRevokeCount = 0; + + private revokeErrorCount = 0; + + private usersDroppedCount = 0; + + inc(param: keyof StaleAnonymousUsersCleanerSummary, value = 1) { + this[param] += value; + } + + summarize(): StaleAnonymousUsersCleanerSummary { + return { + feeAllowanceRevokeCount: this.feeAllowanceRevokeCount, + deploymentGrantRevokeCount: this.deploymentGrantRevokeCount, + revokeErrorCount: this.revokeErrorCount, + usersDroppedCount: this.usersDroppedCount + }; + } +} diff --git a/apps/api/src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service.ts b/apps/api/src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service.ts index a714bef4f..1a7799d13 100644 --- a/apps/api/src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service.ts +++ b/apps/api/src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service.ts @@ -7,9 +7,13 @@ import { UserWalletRepository } from "@src/billing/repositories"; import { ManagedUserWalletService } from "@src/billing/services"; import { InjectSentry, Sentry } from "@src/core/providers/sentry.provider"; import { SentryEventService } from "@src/core/services/sentry-event/sentry-event.service"; +import { DryRunOptions } from "@src/core/types/console"; import { UserRepository } from "@src/user/repositories"; +import { StaleAnonymousUsersCleanerSummarizer } from "@src/user/services/stale-anonymous-users-cleaner-summarizer/stale-anonymous-users-cleaner-summarizer.service"; import { UserConfigService } from "@src/user/services/user-config/user-config.service"; +export interface StaleAnonymousUsersCleanerOptions extends DryRunOptions {} + @singleton() export class StaleAnonymousUsersCleanerService { private readonly CONCURRENCY = 10; @@ -25,7 +29,8 @@ export class StaleAnonymousUsersCleanerService { private readonly sentryEventService: SentryEventService ) {} - async cleanUpStaleAnonymousUsers() { + async cleanUpStaleAnonymousUsers(options: StaleAnonymousUsersCleanerOptions) { + const summary = new StaleAnonymousUsersCleanerSummarizer(); await this.userRepository.paginateStaleAnonymousUsers( { inactivityInDays: this.config.get("STALE_ANONYMOUS_USERS_LIVE_IN_DAYS"), limit: this.CONCURRENCY }, async users => { @@ -34,20 +39,30 @@ export class StaleAnonymousUsersCleanerService { const { errors } = await PromisePool.withConcurrency(this.CONCURRENCY) .for(wallets) .process(async wallet => { - await this.managedUserWalletService.revokeAll(wallet.address, "USER_INACTIVITY"); + const result = await this.managedUserWalletService.revokeAll(wallet.address, "USER_INACTIVITY", options); + if (result.feeAllowance) { + summary.inc("feeAllowanceRevokeCount"); + } + if (result.deploymentGrant) { + summary.inc("deploymentGrantRevokeCount"); + } }); const erroredUserIds = errors.map(({ item }) => item.userId); const userIdsToRemove = difference(userIds, erroredUserIds); - if (userIdsToRemove.length) { - await this.userRepository.deleteById(userIdsToRemove); - this.logger.debug({ event: "STALE_ANONYMOUS_USERS_CLEANUP", userIds: userIdsToRemove }); - } - if (errors.length) { + summary.inc("revokeErrorCount", errors.length); this.logger.debug({ event: "STALE_ANONYMOUS_USERS_REVOKE_ERROR", errors }); this.sentry.captureEvent(this.sentryEventService.toEvent(errors)); } + + if (userIdsToRemove.length) { + if (!options.dryRun) { + await this.userRepository.deleteById(userIdsToRemove); + } + summary.inc("usersDroppedCount", userIdsToRemove.length); + } + this.logger.debug({ event: "STALE_ANONYMOUS_USERS_CLEANUP", userIds: userIdsToRemove, summary: summary.summarize(), dryRun: options.dryRun }); } ); } diff --git a/apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts b/apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts index 10f867419..ce63b2147 100644 --- a/apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts +++ b/apps/api/test/functional/stale-anonymous-users-cleanup.spec.ts @@ -54,7 +54,7 @@ describe("Users", () => { const reactivate = walletService.getWalletByUserId(reactivated.user.id, reactivated.token); await reactivate; - await controller.cleanUpStaleAnonymousUsers(); + await controller.cleanUpStaleAnonymousUsers({ dryRun: false }); const [users, wallets] = await Promise.all([userRepository.find(), userWalletRepository.find()]); From 5b13a8534578e724b7e7ecad6bf6e22f14f5bb74 Mon Sep 17 00:00:00 2001 From: CI Date: Mon, 18 Nov 2024 20:05:20 +0000 Subject: [PATCH 10/68] chore(release): released version console-api/v2.31.0-beta.0 --- apps/api/CHANGELOG.md | 8 ++++++++ apps/api/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/api/CHANGELOG.md b/apps/api/CHANGELOG.md index 0679bdcad..391db0b98 100644 --- a/apps/api/CHANGELOG.md +++ b/apps/api/CHANGELOG.md @@ -1,5 +1,13 @@ +## [2.31.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.30.1...console-api/v2.31.0-beta.0) (2024-11-18) + + +### Features + +* **deployment:** implement concurrency option for stale deployments cleaner ([54cae5d](https://github.com/akash-network/console/commit/54cae5d0f3c37dd6fe6623bcc249379f99cad247)) +* **user:** implement dry run and summary logging for stale anonymous users cleaner ([61752e9](https://github.com/akash-network/console/commit/61752e90fecc559eade828c721fa54839d8aef49)), closes [#464](https://github.com/akash-network/console/issues/464) + ## [2.30.1](https://github.com/akash-network/console/compare/console-api/v2.30.1-beta.0...console-api/v2.30.1) (2024-11-15) ## [2.30.1-beta.0](https://github.com/akash-network/console/compare/console-api/v2.30.0...console-api/v2.30.1-beta.0) (2024-11-15) diff --git a/apps/api/package.json b/apps/api/package.json index 9498c4f58..977a66ee2 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-api", - "version": "2.30.1", + "version": "2.31.0-beta.0", "description": "Api providing data to the deploy tool", "repository": { "type": "git", From 3c4b197bcb947212cffbff531d6a6f61248e19c4 Mon Sep 17 00:00:00 2001 From: CI Date: Mon, 18 Nov 2024 20:11:18 +0000 Subject: [PATCH 11/68] chore(release): released version console-web/v2.23.0-beta.0 --- apps/deploy-web/CHANGELOG.md | 7 +++++++ apps/deploy-web/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/deploy-web/CHANGELOG.md b/apps/deploy-web/CHANGELOG.md index d8cac7b23..dcf772ed0 100644 --- a/apps/deploy-web/CHANGELOG.md +++ b/apps/deploy-web/CHANGELOG.md @@ -1,5 +1,12 @@ +## [2.23.0-beta.0](https://github.com/akash-network/console/compare/console-web/v2.22.1...console-web/v2.23.0-beta.0) (2024-11-18) + + +### Features + +* **deployment:** implement concurrency option for stale deployments cleaner ([54cae5d](https://github.com/akash-network/console/commit/54cae5d0f3c37dd6fe6623bcc249379f99cad247)) + ## [2.22.1](https://github.com/akash-network/console/compare/console-web/v2.22.1-beta.0...console-web/v2.22.1) (2024-11-14) ## [2.22.1-beta.0](https://github.com/akash-network/console/compare/console-web/v2.22.0...console-web/v2.22.1-beta.0) (2024-11-14) diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index bac97030f..509c41d0d 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-web", - "version": "2.22.1", + "version": "2.23.0-beta.0", "private": true, "description": "Web UI to deploy on the Akash Network and view statistic about network usage.", "license": "Apache-2.0", From 552cd8244634bf1de49875ce0d9b7490466ae5b0 Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Tue, 19 Nov 2024 10:20:32 +0100 Subject: [PATCH 12/68] feat(analytics): add user analytics and refactor analytic related logic --- .commitlintrc.json | 3 +- apps/api/package.json | 6 +-- apps/api/src/app.ts | 31 +++++++--------- apps/api/src/index.ts | 13 +++---- apps/api/src/routers/userRouter.ts | 7 +--- apps/api/src/services/db/userDataService.ts | 8 +--- apps/deploy-web/package.json | 1 + .../authorizations/AllowanceModal.tsx | 4 +- .../components/authorizations/GrantModal.tsx | 4 +- .../deployments/DeploymentDepositModal.tsx | 4 +- .../deployments/DeploymentDetail.tsx | 4 +- .../deployments/DeploymentDetailTopBar.tsx | 6 +-- .../deployments/DeploymentListRow.tsx | 6 +-- .../components/deployments/DeploymentLogs.tsx | 4 +- .../components/deployments/ManifestUpdate.tsx | 4 +- .../deployments/ShellDownloadModal.tsx | 4 +- .../layout/CustomGoogleAnalytics.tsx | 2 +- .../src/components/liquidity-modal/index.tsx | 6 +-- .../components/new-deployment/CreateLease.tsx | 10 ++--- .../new-deployment/ManifestEdit.tsx | 6 +-- .../src/components/sdl/ImportSdlModal.tsx | 4 +- .../src/components/sdl/RentGpusForm.tsx | 4 +- .../src/components/sdl/SaveTemplateModal.tsx | 6 +-- .../components/sdl/SimpleSdlBuilderForm.tsx | 12 +++--- .../components/settings/ExportCertificate.tsx | 2 +- .../src/components/templates/UserTemplate.tsx | 16 ++++---- .../src/components/user/UserFavorites.tsx | 4 +- .../src/components/user/UserProfile.tsx | 6 +-- .../src/components/user/UserProfileLayout.tsx | 4 +- .../src/components/user/UserSettingsForm.tsx | 4 +- .../src/config/browser-env.config.ts | 5 +-- .../CertificateProviderContext.tsx | 10 ++--- .../context/WalletProvider/WalletProvider.tsx | 10 ++--- apps/deploy-web/src/hooks/useUser.ts | 13 ++++++- apps/deploy-web/src/pages/_app.tsx | 2 +- .../services/analytics/analytics.service.ts | 13 +++++++ .../src/services/http/http.service.ts | 9 +++++ .../managed-wallet-http.service.ts | 10 +++++ .../src/{utils => types}/analytics.ts | 37 ++++++++----------- package-lock.json | 14 +++++-- 40 files changed, 172 insertions(+), 146 deletions(-) create mode 100644 apps/deploy-web/src/services/analytics/analytics.service.ts rename apps/deploy-web/src/{utils => types}/analytics.ts (61%) diff --git a/.commitlintrc.json b/.commitlintrc.json index c3a5e516e..60b0b0b68 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -20,7 +20,8 @@ "release", "repo", "styling", - "observability" + "observability", + "analytics" ] ] } diff --git a/apps/api/package.json b/apps/api/package.json index 977a66ee2..ae92fb428 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -21,6 +21,7 @@ "migrate": "node-pg-migrate", "migration:gen": "drizzle-kit generate", "prod": "doppler run -- node dist/server.js", + "release": "release-it", "start": "webpack --config webpack.dev.js --watch", "test": "jest --selectProjects unit functional", "test:cov": "jest --selectProjects unit functional --coverage", @@ -30,8 +31,7 @@ "test:unit": "jest --selectProjects unit", "test:unit:cov": "jest --selectProjects unit --coverage", "test:unit:watch": "jest --selectProjects unit --watch", - "test:watch": "jest --selectProjects unit functional --watch", - "release": "release-it" + "test:watch": "jest --selectProjects unit functional --watch" }, "dependencies": { "@akashnetwork/akash-api": "^1.3.0", @@ -59,7 +59,6 @@ "@opentelemetry/sdk-node": "^0.54.0", "@sentry/node": "^7.55.2", "@supercharge/promise-pool": "^3.2.0", - "@types/jsonwebtoken": "^9.0.6", "@ucast/core": "^1.10.2", "async-sema": "^3.1.1", "axios": "^1.7.2", @@ -104,6 +103,7 @@ "@types/http-assert": "^1.5.5", "@types/http-errors": "^2.0.4", "@types/jest": "^29.5.12", + "@types/jsonwebtoken": "^9.0.6", "@types/lodash": "^4.17.0", "@types/memory-cache": "^0.2.2", "@types/node": "20.14.0", diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 4f33addd8..e3539724b 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -7,6 +7,7 @@ import { Context, Hono, Next } from "hono"; import { cors } from "hono/cors"; import { container } from "tsyringe"; +import { AuthInterceptor } from "@src/auth/services/auth.interceptor"; import { config } from "@src/core/config"; import { getSentry, sentryOptions } from "@src/core/providers/sentry.provider"; import { HonoErrorHandlerService } from "@src/core/services/hono-error-handler/hono-error-handler.service"; @@ -23,7 +24,9 @@ import { userRouter } from "./routers/userRouter"; import { web3IndexRouter } from "./routers/web3indexRouter"; import { env } from "./utils/env"; import { bytesToHumanReadableSize } from "./utils/files"; +import { checkoutRouter, getWalletListRouter, signAndBroadcastTxRouter, startTrialRouter, stripeWebhook } from "./billing"; import { Scheduler } from "./scheduler"; +import { createAnonymousUserRouter, getAnonymousUserRouter } from "./user"; const appHono = new Hono(); appHono.use( @@ -33,7 +36,7 @@ appHono.use( }) ); -const { PORT = 3080, BILLING_ENABLED } = process.env; +const { PORT = 3080 } = process.env; const scheduler = new Scheduler({ healthchecksEnabled: env.HEALTHCHECKS_ENABLED === "true", @@ -45,6 +48,7 @@ const scheduler = new Scheduler({ appHono.use(container.resolve(HttpLoggerService).intercept()); appHono.use(container.resolve(RequestContextInterceptor).intercept()); +appHono.use(container.resolve(AuthInterceptor).intercept()); appHono.use("*", async (c: Context, next: Next) => { const { sentry } = await import("@hono/sentry"); return sentry({ @@ -63,23 +67,14 @@ appHono.route("/web3-index", web3IndexRouter); appHono.route("/dashboard", dashboardRouter); appHono.route("/internal", internalRouter); -// TODO: remove condition once billing is in prod -if (BILLING_ENABLED === "true") { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { AuthInterceptor } = require("./auth/services/auth.interceptor"); - appHono.use(container.resolve(AuthInterceptor).intercept()); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { startTrialRouter, getWalletListRouter, signAndBroadcastTxRouter, checkoutRouter, stripeWebhook } = require("./billing"); - appHono.route("/", startTrialRouter); - appHono.route("/", getWalletListRouter); - appHono.route("/", signAndBroadcastTxRouter); - appHono.route("/", checkoutRouter); - appHono.route("/", stripeWebhook); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { createAnonymousUserRouter, getAnonymousUserRouter } = require("./user"); - appHono.route("/", createAnonymousUserRouter); - appHono.route("/", getAnonymousUserRouter); -} +appHono.route("/", startTrialRouter); +appHono.route("/", getWalletListRouter); +appHono.route("/", signAndBroadcastTxRouter); +appHono.route("/", checkoutRouter); +appHono.route("/", stripeWebhook); + +appHono.route("/", createAnonymousUserRouter); +appHono.route("/", getAnonymousUserRouter); appHono.get("/status", c => { const version = packageJson.version; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index c058d6fbc..fba41b0de 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -2,15 +2,12 @@ import "reflect-metadata"; import "@akashnetwork/env-loader"; import "./open-telemetry"; -async function bootstrap() { - /* eslint-disable @typescript-eslint/no-var-requires */ - if (process.env.BILLING_ENABLED === "true") { - const pg = require("./core"); - await pg.migratePG(); - } +import { initApp } from "./app"; +import { migratePG } from "./core"; - const entry = require("./app"); - await entry.initApp(); +async function bootstrap() { + await migratePG(); + await initApp(); } bootstrap(); diff --git a/apps/api/src/routers/userRouter.ts b/apps/api/src/routers/userRouter.ts index 57a36ce52..970f797f8 100644 --- a/apps/api/src/routers/userRouter.ts +++ b/apps/api/src/routers/userRouter.ts @@ -3,6 +3,7 @@ import assert from "http-assert"; import { container } from "tsyringe"; import * as uuid from "uuid"; +import { AuthTokenService } from "@src/auth/services/auth-token/auth-token.service"; import { getCurrentUserId, optionalUserMiddleware, requiredUserMiddleware } from "@src/middlewares/userMiddleware"; import { addTemplateFavorite, @@ -53,12 +54,6 @@ userRequiredRouter.post("/tokenInfo", async c => { }); async function extractAnonymousUserId(c: Context) { - if (process.env.BILLING_ENABLED !== "true") { - return; - } - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { AuthTokenService } = require("@src/auth/services/auth-token/auth-token.service"); - const anonymousBearer = c.req.header("x-anonymous-authorization"); if (anonymousBearer) { diff --git a/apps/api/src/services/db/userDataService.ts b/apps/api/src/services/db/userDataService.ts index ccfda6e13..52f9e35f9 100644 --- a/apps/api/src/services/db/userDataService.ts +++ b/apps/api/src/services/db/userDataService.ts @@ -4,6 +4,8 @@ import pick from "lodash/pick"; import { Transaction } from "sequelize"; import { container } from "tsyringe"; +import { UserWalletRepository } from "@src/billing/repositories"; + const logger = LoggerService.forContext("UserDataService"); function randomIntFromInterval(min: number, max: number) { @@ -150,12 +152,6 @@ export async function getSettingsOrInit({ anonymousUserId, userId, wantedUsernam } async function tryToTransferWallet(prevUserId: string, nextUserId: string) { - if (process.env.BILLING_ENABLED !== "true") { - return; - } - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { UserWalletRepository } = require("@src/billing/repositories/user-wallet/user-wallet.repository"); - const userWalletRepository = container.resolve(UserWalletRepository); try { diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index 509c41d0d..af64d6139 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -123,6 +123,7 @@ "@release-it/conventional-changelog": "github:akash-network/conventional-changelog#feature/pre-release", "@types/auth0": "^2.35.3", "@types/file-saver": "^2.0.5", + "@types/gtag.js": "^0.0.20", "@types/js-yaml": "^4.0.5", "@types/json2csv": "^5.0.3", "@types/lodash": "^4.14.197", diff --git a/apps/deploy-web/src/components/authorizations/AllowanceModal.tsx b/apps/deploy-web/src/components/authorizations/AllowanceModal.tsx index 1f817ebfe..e33a1452f 100644 --- a/apps/deploy-web/src/components/authorizations/AllowanceModal.tsx +++ b/apps/deploy-web/src/components/authorizations/AllowanceModal.tsx @@ -13,8 +13,8 @@ import { LinkTo } from "@src/components/shared/LinkTo"; import { UAKT_DENOM } from "@src/config/denom.config"; import { useWallet } from "@src/context/WalletProvider"; import { useDenomData } from "@src/hooks/useWalletBalance"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { AllowanceType } from "@src/types/grant"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { aktToUakt, coinToDenom } from "@src/utils/priceUtils"; import { TransactionMessageData } from "@src/utils/TransactionMessageData"; @@ -69,7 +69,7 @@ export const AllowanceModal: React.FunctionComponent = ({ editingAllowanc if (response) { event(AnalyticsEvents.AUTHORIZE_SPEND, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Authorize wallet to spend on deployment deposits" }); diff --git a/apps/deploy-web/src/components/authorizations/GrantModal.tsx b/apps/deploy-web/src/components/authorizations/GrantModal.tsx index 2199f9821..cb56b2616 100644 --- a/apps/deploy-web/src/components/authorizations/GrantModal.tsx +++ b/apps/deploy-web/src/components/authorizations/GrantModal.tsx @@ -27,8 +27,8 @@ import { UAKT_DENOM } from "@src/config/denom.config"; import { useWallet } from "@src/context/WalletProvider"; import { getUsdcDenom, useUsdcDenom } from "@src/hooks/useDenom"; import { useDenomData } from "@src/hooks/useWalletBalance"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { GrantType } from "@src/types/grant"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { denomToUdenom } from "@src/utils/mathHelpers"; import { aktToUakt, coinToDenom } from "@src/utils/priceUtils"; import { TransactionMessageData } from "@src/utils/TransactionMessageData"; @@ -90,7 +90,7 @@ export const GrantModal: React.FunctionComponent = ({ editingGrant, addre if (response) { event(AnalyticsEvents.AUTHORIZE_SPEND, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Authorize wallet to spend on deployment deposits" }); diff --git a/apps/deploy-web/src/components/deployments/DeploymentDepositModal.tsx b/apps/deploy-web/src/components/deployments/DeploymentDepositModal.tsx index 6618fd40b..bb2377762 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentDepositModal.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentDepositModal.tsx @@ -32,7 +32,7 @@ import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useDenomData, useWalletBalance } from "@src/hooks/useWalletBalance"; import { useGranteeGrants } from "@src/queries/useGrantsQuery"; -import { AnalyticsEvents } from "@src/utils/analytics"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { denomToUdenom, udenomToDenom } from "@src/utils/mathHelpers"; import { coinToUDenom } from "@src/utils/priceUtils"; import { LinkTo } from "../shared/LinkTo"; @@ -189,7 +189,7 @@ export const DeploymentDepositModal: React.FunctionComponent depositData?.balance) { diff --git a/apps/deploy-web/src/components/deployments/DeploymentDetail.tsx b/apps/deploy-web/src/components/deployments/DeploymentDetail.tsx index a1d545f85..73040c9a5 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentDetail.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentDetail.tsx @@ -18,8 +18,8 @@ import { useDeploymentDetail } from "@src/queries/useDeploymentQuery"; import { useDeploymentLeaseList } from "@src/queries/useLeaseQuery"; import { useProviderList } from "@src/queries/useProvidersQuery"; import { extractRepositoryUrl, isImageInYaml } from "@src/services/remote-deploy/remote-deployment-controller.service"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { RouteStep } from "@src/types/route-steps.type"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { getDeploymentLocalData } from "@src/utils/deploymentLocalDataUtils"; import { UrlService } from "@src/utils/urlUtils"; import Layout from "../layout/Layout"; @@ -135,7 +135,7 @@ export function DeploymentDetail({ dseq }: React.PropsWithChildren<{ dseq: strin } event(`${AnalyticsEvents.NAVIGATE_TAB}${value}`, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: `Navigate tab ${value} in deployment detail` }); }; diff --git a/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar.tsx b/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar.tsx index afdf62cd8..c1a9778b6 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar.tsx @@ -11,8 +11,8 @@ import { useLocalNotes } from "@src/context/LocalNoteProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useManagedDeploymentConfirm } from "@src/hooks/useManagedDeploymentConfirm"; import { usePreviousRoute } from "@src/hooks/usePreviousRoute"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { DeploymentDto } from "@src/types/deployment"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { TransactionMessageData } from "@src/utils/TransactionMessageData"; import { UrlService } from "@src/utils/urlUtils"; import { DeploymentDepositModal } from "./DeploymentDepositModal"; @@ -59,7 +59,7 @@ export const DeploymentDetailTopBar: React.FunctionComponent = ({ address loadDeploymentDetail(); event(AnalyticsEvents.CLOSE_DEPLOYMENT, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Close deployment in deployment detail" }); } @@ -83,7 +83,7 @@ export const DeploymentDetailTopBar: React.FunctionComponent = ({ address loadDeploymentDetail(); event(AnalyticsEvents.DEPLOYMENT_DEPOSIT, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Deposit deployment in deployment detail" }); } diff --git a/apps/deploy-web/src/components/deployments/DeploymentListRow.tsx b/apps/deploy-web/src/components/deployments/DeploymentListRow.tsx index eebd065bc..63a723727 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentListRow.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentListRow.tsx @@ -25,9 +25,9 @@ import { useManagedDeploymentConfirm } from "@src/hooks/useManagedDeploymentConf import { getShortText } from "@src/hooks/useShortText"; import { useDenomData } from "@src/hooks/useWalletBalance"; import { useAllLeases } from "@src/queries/useLeaseQuery"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { NamedDeploymentDto } from "@src/types/deployment"; import { ApiProviderList } from "@src/types/provider"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { udenomToDenom } from "@src/utils/mathHelpers"; import { getAvgCostPerMonth, getTimeLeft, useRealTimeLeft } from "@src/utils/priceUtils"; import { TransactionMessageData } from "@src/utils/TransactionMessageData"; @@ -138,7 +138,7 @@ export const DeploymentListRow: React.FunctionComponent = ({ deployment, refreshDeployments(); event(AnalyticsEvents.DEPLOYMENT_DEPOSIT, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Deposit to deployment from list" }); } @@ -163,7 +163,7 @@ export const DeploymentListRow: React.FunctionComponent = ({ deployment, refreshDeployments(); event(AnalyticsEvents.CLOSE_DEPLOYMENT, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Close deployment from list" }); } diff --git a/apps/deploy-web/src/components/deployments/DeploymentLogs.tsx b/apps/deploy-web/src/components/deployments/DeploymentLogs.tsx index b8b2b9dcb..eb25712ca 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentLogs.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentLogs.tsx @@ -21,8 +21,8 @@ import { useCertificate } from "@src/context/CertificateProvider"; import { useThrottledCallback } from "@src/hooks/useThrottle"; import { useLeaseStatus } from "@src/queries/useLeaseQuery"; import { useProviderList } from "@src/queries/useProvidersQuery"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { LeaseDto } from "@src/types/deployment"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { LeaseSelect } from "./LeaseSelect"; export type LOGS_MODE = "logs" | "events"; @@ -231,7 +231,7 @@ export const DeploymentLogs: React.FunctionComponent = ({ leases, selecte await downloadLogs(providerInfo.hostUri, selectedLease.dseq, selectedLease.gseq, selectedLease.oseq, isLogs); event(AnalyticsEvents.DOWNLOADED_LOGS, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: isLogs ? "Downloaded deployment logs" : "Downloaded deployment events" }); diff --git a/apps/deploy-web/src/components/deployments/ManifestUpdate.tsx b/apps/deploy-web/src/components/deployments/ManifestUpdate.tsx index e36adeaa1..91b98519b 100644 --- a/apps/deploy-web/src/components/deployments/ManifestUpdate.tsx +++ b/apps/deploy-web/src/components/deployments/ManifestUpdate.tsx @@ -15,9 +15,9 @@ import { LocalCert } from "@src/context/CertificateProvider/CertificateProviderC import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useProviderList } from "@src/queries/useProvidersQuery"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { DeploymentDto, LeaseDto } from "@src/types/deployment"; import { ApiProviderList } from "@src/types/provider"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { deploymentData } from "@src/utils/deploymentData"; import { getDeploymentLocalData, saveDeploymentManifest } from "@src/utils/deploymentLocalDataUtils"; import { sendManifestToProvider } from "@src/utils/deploymentUtils"; @@ -168,7 +168,7 @@ export const ManifestUpdate: React.FunctionComponent = ({ } event(AnalyticsEvents.UPDATE_DEPLOYMENT, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Update deployment" }); diff --git a/apps/deploy-web/src/components/deployments/ShellDownloadModal.tsx b/apps/deploy-web/src/components/deployments/ShellDownloadModal.tsx index d4419214c..d6f73ac7c 100644 --- a/apps/deploy-web/src/components/deployments/ShellDownloadModal.tsx +++ b/apps/deploy-web/src/components/deployments/ShellDownloadModal.tsx @@ -7,7 +7,7 @@ import { event } from "nextjs-google-analytics"; import { z } from "zod"; import { useBackgroundTask } from "@src/context/BackgroundTaskProvider"; -import { AnalyticsEvents } from "@src/utils/analytics"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; const formSchema = z.object({ filePath: z @@ -39,7 +39,7 @@ export const ShellDownloadModal = ({ selectedLease, onCloseClick, selectedServic downloadFileFromShell(providerInfo.hostUri, selectedLease.dseq, selectedLease.gseq, selectedLease.oseq, selectedService, filePath); event(AnalyticsEvents.DOWNLOADED_SHELL_FILE, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Download file from shell" }); diff --git a/apps/deploy-web/src/components/layout/CustomGoogleAnalytics.tsx b/apps/deploy-web/src/components/layout/CustomGoogleAnalytics.tsx index 3fe30e5e2..895ea9d78 100644 --- a/apps/deploy-web/src/components/layout/CustomGoogleAnalytics.tsx +++ b/apps/deploy-web/src/components/layout/CustomGoogleAnalytics.tsx @@ -13,5 +13,5 @@ export default function GoogleAnalytics() { nonInteraction: true // avoids affecting bounce rate. }); }); - return <>{browserEnvConfig.NEXT_PUBLIC_NODE_ENV === "production" && }; + return <>{!!browserEnvConfig.NEXT_PUBLIC_GA_MEASUREMENT_ID && }; } diff --git a/apps/deploy-web/src/components/liquidity-modal/index.tsx b/apps/deploy-web/src/components/liquidity-modal/index.tsx index 68f79ca88..4f368211f 100644 --- a/apps/deploy-web/src/components/liquidity-modal/index.tsx +++ b/apps/deploy-web/src/components/liquidity-modal/index.tsx @@ -8,14 +8,14 @@ import { Modal } from "@mui/material"; import { event } from "nextjs-google-analytics"; import { useWallet } from "@src/context/WalletProvider"; -import { AnalyticsEvents } from "@src/utils/analytics"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; export type NonUndefined = T extends undefined ? never : T; const ToggleLiquidityModalButton: React.FC<{ onClick: () => void }> = ({ onClick }) => { const _onClick = () => { event(AnalyticsEvents.LEAP_GET_MORE_TOKENS, { - category: "wallet", + category: AnalyticsCategory.WALLET, label: "Open Leap liquidity modal" }); @@ -135,7 +135,7 @@ const LiquidityModal: React.FC = ({ refreshBalances }) => { onTxnComplete: () => { refreshBalances(); event(AnalyticsEvents.LEAP_TRANSACTION_COMPLETE, { - category: "wallet", + category: AnalyticsCategory.WALLET, label: "Completed a transaction on Leap liquidity modal" }); } diff --git a/apps/deploy-web/src/components/new-deployment/CreateLease.tsx b/apps/deploy-web/src/components/new-deployment/CreateLease.tsx index 9bd06bdd0..4a2eff3b6 100644 --- a/apps/deploy-web/src/components/new-deployment/CreateLease.tsx +++ b/apps/deploy-web/src/components/new-deployment/CreateLease.tsx @@ -2,8 +2,8 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Alert, - AlertTitle, AlertDescription, + AlertTitle, Button, Checkbox, CustomTooltip, @@ -16,6 +16,7 @@ import { } from "@akashnetwork/ui/components"; import { ArrowRight, BadgeCheck, Bin, InfoCircle, MoreHoriz, Xmark } from "iconoir-react"; import yaml from "js-yaml"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { event } from "nextjs-google-analytics"; import { useSnackbar } from "notistack"; @@ -27,9 +28,9 @@ import { useWhen } from "@src/hooks/useWhen"; import { useBidList } from "@src/queries/useBidQuery"; import { useDeploymentDetail } from "@src/queries/useDeploymentQuery"; import { useProviderList } from "@src/queries/useProvidersQuery"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { BidDto } from "@src/types/deployment"; import { RouteStep } from "@src/types/route-steps.type"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { deploymentData } from "@src/utils/deploymentData"; import { getDeploymentLocalData } from "@src/utils/deploymentLocalDataUtils"; import { sendManifestToProvider } from "@src/utils/deploymentUtils"; @@ -44,7 +45,6 @@ import { ManifestErrorSnackbar } from "../shared/ManifestErrorSnackbar"; import ViewPanel from "../shared/ViewPanel"; import { BidCountdownTimer } from "./BidCountdownTimer"; import { BidGroup } from "./BidGroup"; -import Link from "next/link"; type Props = { dseq: string; @@ -111,7 +111,7 @@ export const CreateLease: React.FunctionComponent = ({ dseq }) => { const localDeploymentData = getDeploymentLocalData(dseq); event(AnalyticsEvents.SEND_MANIFEST, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Send manifest after creating lease" }); @@ -201,7 +201,7 @@ export const CreateLease: React.FunctionComponent = ({ dseq }) => { if (!response) throw new Error("Rejected transaction"); event(AnalyticsEvents.CREATE_LEASE, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Create lease" }); await sendManifest(); diff --git a/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx b/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx index d1d73a840..e50d98e3b 100644 --- a/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx +++ b/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx @@ -23,10 +23,11 @@ import { useWhen } from "@src/hooks/useWhen"; import { useDepositParams } from "@src/queries/useSettings"; import sdlStore from "@src/store/sdlStore"; import { TemplateCreation } from "@src/types"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import type { DepositParams } from "@src/types/deployment"; import { RouteStep } from "@src/types/route-steps.type"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { deploymentData } from "@src/utils/deploymentData"; +import { appendTrialAttribute } from "@src/utils/deploymentData/v1beta3"; import { saveDeploymentManifestAndName } from "@src/utils/deploymentLocalDataUtils"; import { validateDeploymentData } from "@src/utils/deploymentUtils"; import { importSimpleSdl } from "@src/utils/sdl/sdlImport"; @@ -42,7 +43,6 @@ import { LinkTo } from "../shared/LinkTo"; import { PrerequisiteList } from "../shared/PrerequisiteList"; import ViewPanel from "../shared/ViewPanel"; import { SdlBuilder, SdlBuilderRefType } from "./SdlBuilder"; -import { appendTrialAttribute } from "@src/utils/deploymentData/v1beta3"; type Props = { onTemplateSelected: Dispatch; @@ -286,7 +286,7 @@ export const ManifestEdit: React.FunctionComponent = ({ router.replace(UrlService.newDeployment({ step: RouteStep.createLeases, dseq: dd.deploymentId.dseq })); event(AnalyticsEvents.CREATE_DEPLOYMENT, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Create deployment in wizard" }); } else { diff --git a/apps/deploy-web/src/components/sdl/ImportSdlModal.tsx b/apps/deploy-web/src/components/sdl/ImportSdlModal.tsx index 2880a3405..2d97c7276 100644 --- a/apps/deploy-web/src/components/sdl/ImportSdlModal.tsx +++ b/apps/deploy-web/src/components/sdl/ImportSdlModal.tsx @@ -9,7 +9,7 @@ import { event } from "nextjs-google-analytics"; import { useSnackbar } from "notistack"; import { SdlBuilderFormValuesType, ServiceType } from "@src/types"; -import { AnalyticsEvents } from "@src/utils/analytics"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { importSimpleSdl } from "@src/utils/sdl/sdlImport"; import { Timer } from "@src/utils/timer"; @@ -75,7 +75,7 @@ export const ImportSdlModal: React.FunctionComponent = ({ onClose, setVal }); event(AnalyticsEvents.IMPORT_SDL, { - category: "sdl_builder", + category: AnalyticsCategory.SDL_BUILDER, label: "Import SDL" }); diff --git a/apps/deploy-web/src/components/sdl/RentGpusForm.tsx b/apps/deploy-web/src/components/sdl/RentGpusForm.tsx index c423ce759..a16bdc7a0 100644 --- a/apps/deploy-web/src/components/sdl/RentGpusForm.tsx +++ b/apps/deploy-web/src/components/sdl/RentGpusForm.tsx @@ -22,10 +22,10 @@ import { useGpuModels } from "@src/queries/useGpuQuery"; import { useDepositParams } from "@src/queries/useSettings"; import sdlStore from "@src/store/sdlStore"; import { ApiTemplate, ProfileGpuModelType, RentGpusFormValuesSchema, RentGpusFormValuesType, ServiceType } from "@src/types"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { DepositParams } from "@src/types/deployment"; import { ProviderAttributeSchemaDetailValue } from "@src/types/providerAttributes"; import { RouteStep } from "@src/types/route-steps.type"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { deploymentData } from "@src/utils/deploymentData"; import { saveDeploymentManifestAndName } from "@src/utils/deploymentLocalDataUtils"; import { validateDeploymentData } from "@src/utils/deploymentUtils"; @@ -275,7 +275,7 @@ export const RentGpusForm: React.FunctionComponent = () => { router.push(UrlService.newDeployment({ step: RouteStep.createLeases, dseq: dd.deploymentId.dseq })); event(AnalyticsEvents.CREATE_GPU_DEPLOYMENT, { - category: "deployments", + category: AnalyticsCategory.DEPLOYMENTS, label: "Create deployment rent gpu form" }); } else { diff --git a/apps/deploy-web/src/components/sdl/SaveTemplateModal.tsx b/apps/deploy-web/src/components/sdl/SaveTemplateModal.tsx index 55218cb2e..70e123984 100644 --- a/apps/deploy-web/src/components/sdl/SaveTemplateModal.tsx +++ b/apps/deploy-web/src/components/sdl/SaveTemplateModal.tsx @@ -12,7 +12,7 @@ import { useCustomUser } from "@src/hooks/useCustomUser"; import { getShortText } from "@src/hooks/useShortText"; import { useSaveUserTemplate } from "@src/queries/useTemplateQuery"; import { EnvironmentVariableType, ITemplate, ServiceType } from "@src/types"; -import { AnalyticsEvents } from "@src/utils/analytics"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; type Props = { services: ServiceType[]; @@ -76,12 +76,12 @@ export const SaveTemplateModal: React.FunctionComponent = ({ onClose, get if (newTemplateMetadata.id) { event(AnalyticsEvents.UPDATE_SDL_TEMPLATE, { - category: "sdl_builder", + category: AnalyticsCategory.SDL_BUILDER, label: "Update SDL template" }); } else { event(AnalyticsEvents.CREATE_SDL_TEMPLATE, { - category: "sdl_builder", + category: AnalyticsCategory.SDL_BUILDER, label: "Create SDL template" }); } diff --git a/apps/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx b/apps/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx index 1de14f056..feddbb59f 100644 --- a/apps/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx +++ b/apps/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx @@ -17,9 +17,9 @@ import useFormPersist from "@src/hooks/useFormPersist"; import { useGpuModels } from "@src/queries/useGpuQuery"; import sdlStore from "@src/store/sdlStore"; import { ITemplate, SdlBuilderFormValuesSchema, SdlBuilderFormValuesType, ServiceType } from "@src/types"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { RouteStep } from "@src/types/route-steps.type"; import { memoryUnits, storageUnits } from "@src/utils/akash/units"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { defaultService } from "@src/utils/sdl/data"; import { generateSdl } from "@src/utils/sdl/sdlGenerator"; import { importSimpleSdl } from "@src/utils/sdl/sdlImport"; @@ -141,7 +141,7 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = () => { router.push(UrlService.newDeployment({ step: RouteStep.editDeployment })); event(AnalyticsEvents.DEPLOY_SDL, { - category: "sdl_builder", + category: AnalyticsCategory.SDL_BUILDER, label: "Deploy SDL from create page" }); } catch (error) { @@ -166,7 +166,7 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = () => { setIsPreviewingSdl(true); event(AnalyticsEvents.PREVIEW_SDL, { - category: "sdl_builder", + category: AnalyticsCategory.SDL_BUILDER, label: "Preview SDL from create page" }); } catch (error) { @@ -225,7 +225,7 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = () => { { event(AnalyticsEvents.CLICK_SDL_PROFILE, { - category: "sdl_builder", + category: AnalyticsCategory.SDL_BUILDER, label: "Click on SDL user profile" }); }} @@ -241,7 +241,7 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = () => { className="inline-flex cursor-pointer items-center" onClick={() => { event(AnalyticsEvents.CLICK_VIEW_TEMPLATE, { - category: "sdl_builder", + category: AnalyticsCategory.SDL_BUILDER, label: "Click on view SDL template" }); }} @@ -272,7 +272,7 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = () => { type="button" onClick={() => { event(AnalyticsEvents.RESET_SDL, { - category: "sdl_builder", + category: AnalyticsCategory.SDL_BUILDER, label: "Reset SDL" }); diff --git a/apps/deploy-web/src/components/settings/ExportCertificate.tsx b/apps/deploy-web/src/components/settings/ExportCertificate.tsx index 5971f0f7b..a81e90529 100644 --- a/apps/deploy-web/src/components/settings/ExportCertificate.tsx +++ b/apps/deploy-web/src/components/settings/ExportCertificate.tsx @@ -4,7 +4,7 @@ import { Alert, Popup } from "@akashnetwork/ui/components"; import { event } from "nextjs-google-analytics"; import { CodeSnippet } from "@src/components/shared/CodeSnippet"; -import { AnalyticsEvents } from "@src/utils/analytics"; +import { AnalyticsEvents } from "@src/types/analytics"; import { useSelectedWalletFromStorage } from "@src/utils/walletUtils"; export function ExportCertificate({ isOpen, onClose }: React.PropsWithChildren<{ isOpen: boolean; onClose: () => void }>) { diff --git a/apps/deploy-web/src/components/templates/UserTemplate.tsx b/apps/deploy-web/src/components/templates/UserTemplate.tsx index cb116c8d9..d27531708 100644 --- a/apps/deploy-web/src/components/templates/UserTemplate.tsx +++ b/apps/deploy-web/src/components/templates/UserTemplate.tsx @@ -17,8 +17,8 @@ import { getShortText } from "@src/hooks/useShortText"; import { useDeleteTemplate } from "@src/queries/useTemplateQuery"; import sdlStore from "@src/store/sdlStore"; import { ITemplate } from "@src/types"; +import { AnalyticsCategory, AnalyticsEvents } from "@src/types/analytics"; import { RouteStep } from "@src/types/route-steps.type"; -import { AnalyticsEvents } from "@src/utils/analytics"; import { roundDecimal } from "@src/utils/mathHelpers"; import { bytesToShrink } from "@src/utils/unitUtils"; import { domainName, UrlService } from "@src/utils/urlUtils"; @@ -51,7 +51,7 @@ export const UserTemplate: React.FunctionComponent = ({ id, template }) = await deleteTemplate(); event(AnalyticsEvents.DEPLOY_SDL, { - category: "sdl_builder", + category: AnalyticsCategory.SDL_BUILDER, label: "Delete SDL template from detail" }); @@ -67,7 +67,7 @@ export const UserTemplate: React.FunctionComponent = ({ id, template }) = setIsEditingDescription(false); event(AnalyticsEvents.SAVE_SDL_DESCRIPTION, { - category: "sdl_builder", + category: AnalyticsCategory.SDL_BUILDER, label: "Save SDL description" }); }; @@ -110,7 +110,7 @@ export const UserTemplate: React.FunctionComponent = ({ id, template }) = { event(AnalyticsEvents.CLICK_SDL_PROFILE, { - category: "sdl_builder", + category: AnalyticsCategory.SDL_BUILDER, label: "Click on SDL user profile in template detail" }); }} @@ -123,7 +123,7 @@ export const UserTemplate: React.FunctionComponent = ({ id, template }) = */} - +
  • Sign up diff --git a/apps/deploy-web/src/components/layout/WalletStatus.tsx b/apps/deploy-web/src/components/layout/WalletStatus.tsx index c647d110c..e64d58c2d 100644 --- a/apps/deploy-web/src/components/layout/WalletStatus.tsx +++ b/apps/deploy-web/src/components/layout/WalletStatus.tsx @@ -29,13 +29,13 @@ export function WalletStatus() { <> {isWalletLoaded && !isWalletLoading ? ( isWalletConnected ? ( -
    -
    +
    +
    {!!walletBalance && (
    ) : ( -
    +
    {withBilling && !isSignedInWithTrial && }
    diff --git a/apps/deploy-web/src/components/new-deployment/CreateLease.tsx b/apps/deploy-web/src/components/new-deployment/CreateLease.tsx index 4a2eff3b6..73b7aa223 100644 --- a/apps/deploy-web/src/components/new-deployment/CreateLease.tsx +++ b/apps/deploy-web/src/components/new-deployment/CreateLease.tsx @@ -19,7 +19,9 @@ import yaml from "js-yaml"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { event } from "nextjs-google-analytics"; +import { useTheme as useMuiTheme } from "@mui/material/styles"; import { useSnackbar } from "notistack"; +import useMediaQuery from "@mui/material/useMediaQuery"; import { LocalCert } from "@src/context/CertificateProvider/CertificateProviderContext"; import { useWallet } from "@src/context/WalletProvider"; @@ -92,6 +94,8 @@ export const CreateLease: React.FunctionComponent = ({ dseq }) => { return a as { [key: number]: BidDto }; }, {} as any) || {}; const dseqList = Object.keys(groupedBids).map(group => parseInt(group)); + const muiTheme = useMuiTheme(); + const smallScreen = useMediaQuery(muiTheme.breakpoints.down("md")); const allClosed = (bids?.length || 0) > 0 && bids?.every(bid => bid.state === "closed"); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); @@ -399,7 +403,7 @@ export const CreateLease: React.FunctionComponent = ({ dseq }) => { {dseqList.length > 0 && ( - + {dseqList.map((gseq, i) => ( = ({ dseq }) => { {isTrialing && ( - Free Trial! - + Free Trial! +

    You are using a free trial and are limited to only a few providers on the network.

    diff --git a/apps/deploy-web/src/components/new-deployment/NewDeploymentContainer.tsx b/apps/deploy-web/src/components/new-deployment/NewDeploymentContainer.tsx index b015e9150..3b3d8746c 100644 --- a/apps/deploy-web/src/components/new-deployment/NewDeploymentContainer.tsx +++ b/apps/deploy-web/src/components/new-deployment/NewDeploymentContainer.tsx @@ -18,6 +18,7 @@ import { CreateLease } from "./CreateLease"; import { ManifestEdit } from "./ManifestEdit"; import { CustomizedSteppers } from "./Stepper"; import { TemplateList } from "./TemplateList"; +import { USER_TEMPLATE_CODE } from "@src/config/deploy.config"; export const NewDeploymentContainer: FC = () => { const [isGitProviderTemplate, setIsGitProviderTemplate] = useState(false); @@ -68,28 +69,29 @@ export const NewDeploymentContainer: FC = () => { const isCreating = !!activeStep && activeStep > getStepIndexByParam(RouteStep.chooseTemplate); if (!templates || (isCreating && !!editedManifest && !!templateId)) return; - const template = getRedeployTemplate() || getGalleryTemplate(); + const template = getRedeployTemplate() || getGalleryTemplate() || deploySdl; + const isUserTemplate = template?.code === USER_TEMPLATE_CODE; + const isUserTemplateInit = isUserTemplate && !!editedManifest; + if (!template || isUserTemplateInit) return; - if (template) { - setSelectedTemplate(template as TemplateCreation); - setEditedManifest(template.content as string); + setSelectedTemplate(template as TemplateCreation); + setEditedManifest(template.content as string); - if ("config" in template && (template.config?.ssh || (!template.config?.ssh && hasComponent("ssh")))) { - toggleCmp("ssh"); - } - const isRemoteYamlImage = isImageInYaml(template?.content as string, getTemplateById(CI_CD_TEMPLATE_ID)?.deploy); - const queryStep = searchParams?.get("step"); - if (queryStep !== RouteStep.editDeployment) { - if (isRemoteYamlImage) { - setIsGitProviderTemplate(true); - } - - const newParams = isRemoteYamlImage - ? { ...searchParams, step: RouteStep.editDeployment, gitProvider: "github" } - : { ...searchParams, step: RouteStep.editDeployment }; - - router.replace(UrlService.newDeployment(newParams)); + if ("config" in template && (template.config?.ssh || (!template.config?.ssh && hasComponent("ssh")))) { + toggleCmp("ssh"); + } + const isRemoteYamlImage = isImageInYaml(template?.content as string, getTemplateById(CI_CD_TEMPLATE_ID)?.deploy); + const queryStep = searchParams?.get("step"); + if (queryStep !== RouteStep.editDeployment) { + if (isRemoteYamlImage) { + setIsGitProviderTemplate(true); } + + const newParams = isRemoteYamlImage + ? { ...searchParams, step: RouteStep.editDeployment, gitProvider: "github" } + : { ...searchParams, step: RouteStep.editDeployment }; + + router.replace(UrlService.newDeployment(newParams)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [templates, editedManifest, searchParams, router, toggleCmp, hasComponent, activeStep]); @@ -138,11 +140,6 @@ export const NewDeploymentContainer: FC = () => { } } - // Jotai state template - if (deploySdl) { - return deploySdl; - } - return null; }; diff --git a/apps/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx b/apps/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx index feddbb59f..2d9bc13fe 100644 --- a/apps/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx +++ b/apps/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx @@ -27,6 +27,7 @@ import { UrlService } from "@src/utils/urlUtils"; import { ImportSdlModal } from "./ImportSdlModal"; import { PreviewSdl } from "./PreviewSdl"; import { SaveTemplateModal } from "./SaveTemplateModal"; +import { USER_TEMPLATE_CODE } from "@src/config/deploy.config"; const DEFAULT_SERVICES = { services: [{ ...defaultService }] @@ -133,7 +134,7 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = () => { setDeploySdl({ title: "", category: "", - code: "", + code: USER_TEMPLATE_CODE, description: "", content: sdl }); diff --git a/apps/deploy-web/src/components/templates/UserTemplate.tsx b/apps/deploy-web/src/components/templates/UserTemplate.tsx index d27531708..d2629fecb 100644 --- a/apps/deploy-web/src/components/templates/UserTemplate.tsx +++ b/apps/deploy-web/src/components/templates/UserTemplate.tsx @@ -24,6 +24,7 @@ import { bytesToShrink } from "@src/utils/unitUtils"; import { domainName, UrlService } from "@src/utils/urlUtils"; import Layout from "../layout/Layout"; import { CustomNextSeo } from "../shared/CustomNextSeo"; +import { USER_TEMPLATE_CODE } from "@src/config/deploy.config"; type Props = { id: string; @@ -130,7 +131,7 @@ export const UserTemplate: React.FunctionComponent = ({ id, template }) = setDeploySdl({ title: "", category: "", - code: "", + code: USER_TEMPLATE_CODE, description: "", content: template.sdl }); diff --git a/apps/deploy-web/src/config/deploy.config.ts b/apps/deploy-web/src/config/deploy.config.ts new file mode 100644 index 000000000..c1532e9e4 --- /dev/null +++ b/apps/deploy-web/src/config/deploy.config.ts @@ -0,0 +1 @@ +export const USER_TEMPLATE_CODE = "USER_TEMPLATE"; \ No newline at end of file diff --git a/apps/deploy-web/src/services/managed-wallet-http/managed-wallet-http.service.ts b/apps/deploy-web/src/services/managed-wallet-http/managed-wallet-http.service.ts index 3b146cfd6..461e29d44 100644 --- a/apps/deploy-web/src/services/managed-wallet-http/managed-wallet-http.service.ts +++ b/apps/deploy-web/src/services/managed-wallet-http/managed-wallet-http.service.ts @@ -55,7 +55,9 @@ class ManagedWalletHttpService extends ManagedWalletHttpServiceOriginal { url.searchParams.delete("session_id"); url.searchParams.delete("payment-canceled"); url.searchParams.delete("payment-success"); - window.history.replaceState({}, document.title, url.toString()); + const newUrl = url.toString(); + // TODO: remove this when fixed https://github.com/vercel/next.js/discussions/18072#discussioncomment-109059 + window.history.replaceState({ ...window.history.state, as: newUrl, url: newUrl }, document.title, newUrl); this.checkoutSessionId = null; } } diff --git a/apps/stats-web/package.json b/apps/stats-web/package.json index 2616ebbb7..c9a54267f 100644 --- a/apps/stats-web/package.json +++ b/apps/stats-web/package.json @@ -13,7 +13,6 @@ "dependencies": { "@akashnetwork/network-store": "*", "@akashnetwork/ui": "*", - "@akashnetwork/network-store": "*", "@cosmjs/encoding": "^0.32.4", "@json2csv/plainjs": "^7.0.4", "@nivo/line": "^0.87.0", diff --git a/package-lock.json b/package-lock.json index d3ece0f0c..c53ece4de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -286,7 +286,7 @@ "lucide-react": "^0.292.0", "material-ui-popup-state": "^4.0.2", "nanoid": "^3.3.4", - "next": "^14.2.3", + "next": "^14.2.18", "next-nprogress-bar": "^2.1.2", "next-pwa": "^5.6.0", "next-qrcode": "^2.1.0", @@ -7437,9 +7437,10 @@ } }, "node_modules/@next/env": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.15.tgz", - "integrity": "sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==" + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.18.tgz", + "integrity": "sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==", + "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { "version": "14.2.4", @@ -7469,12 +7470,13 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz", - "integrity": "sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.18.tgz", + "integrity": "sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -7484,12 +7486,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz", - "integrity": "sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.18.tgz", + "integrity": "sha512-uJCEjutt5VeJ30jjrHV1VIHCsbMYnEqytQgvREx+DjURd/fmKy15NaVK4aR/u98S1LGTnjq35lRTnRyygglxoA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -7499,12 +7502,13 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz", - "integrity": "sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.18.tgz", + "integrity": "sha512-IL6rU8vnBB+BAm6YSWZewc+qvdL1EaA+VhLQ6tlUc0xp+kkdxQrVqAnh8Zek1ccKHlTDFRyAft0e60gteYmQ4A==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -7514,12 +7518,13 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz", - "integrity": "sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.18.tgz", + "integrity": "sha512-RCaENbIZqKKqTlL8KNd+AZV/yAdCsovblOpYFp0OJ7ZxgLNbV5w23CUU1G5On+0fgafrsGcW+GdMKdFjaRwyYA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -7529,12 +7534,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz", - "integrity": "sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.18.tgz", + "integrity": "sha512-3kmv8DlyhPRCEBM1Vavn8NjyXtMeQ49ID0Olr/Sut7pgzaQTo4h01S7Z8YNE0VtbowyuAL26ibcz0ka6xCTH5g==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -7544,12 +7550,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz", - "integrity": "sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.18.tgz", + "integrity": "sha512-mliTfa8seVSpTbVEcKEXGjC18+TDII8ykW4a36au97spm9XMPqQTpdGPNBJ9RySSFw9/hLuaCMByluQIAnkzlw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -7559,12 +7566,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz", - "integrity": "sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.18.tgz", + "integrity": "sha512-J5g0UFPbAjKYmqS3Cy7l2fetFmWMY9Oao32eUsBPYohts26BdrMUyfCJnZFQkX9npYaHNDOWqZ6uV9hSDPw9NA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -7574,12 +7582,13 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", - "integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.18.tgz", + "integrity": "sha512-Ynxuk4ZgIpdcN7d16ivJdjsDG1+3hTvK24Pp8DiDmIa2+A4CfhJSEHHVndCHok6rnLUzAZD+/UOKESQgTsAZGg==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -7589,12 +7598,13 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz", - "integrity": "sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.18.tgz", + "integrity": "sha512-dtRGMhiU9TN5nyhwzce+7c/4CCeykYS+ipY/4mIrGzJ71+7zNo55ZxCB7cAVuNqdwtYniFNR2c9OFQ6UdFIMcg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -29877,11 +29887,12 @@ } }, "node_modules/next": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.15.tgz", - "integrity": "sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.18.tgz", + "integrity": "sha512-H9qbjDuGivUDEnK6wa+p2XKO+iMzgVgyr9Zp/4Iv29lKa+DYaxJGjOeEA+5VOvJh/M7HLiskehInSa0cWxVXUw==", + "license": "MIT", "dependencies": { - "@next/env": "14.2.15", + "@next/env": "14.2.18", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -29896,15 +29907,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.15", - "@next/swc-darwin-x64": "14.2.15", - "@next/swc-linux-arm64-gnu": "14.2.15", - "@next/swc-linux-arm64-musl": "14.2.15", - "@next/swc-linux-x64-gnu": "14.2.15", - "@next/swc-linux-x64-musl": "14.2.15", - "@next/swc-win32-arm64-msvc": "14.2.15", - "@next/swc-win32-ia32-msvc": "14.2.15", - "@next/swc-win32-x64-msvc": "14.2.15" + "@next/swc-darwin-arm64": "14.2.18", + "@next/swc-darwin-x64": "14.2.18", + "@next/swc-linux-arm64-gnu": "14.2.18", + "@next/swc-linux-arm64-musl": "14.2.18", + "@next/swc-linux-x64-gnu": "14.2.18", + "@next/swc-linux-x64-musl": "14.2.18", + "@next/swc-win32-arm64-msvc": "14.2.18", + "@next/swc-win32-ia32-msvc": "14.2.18", + "@next/swc-win32-x64-msvc": "14.2.18" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/packages/ui/components/custom/file-button.tsx b/packages/ui/components/custom/file-button.tsx index 3c22afdff..c848ef941 100644 --- a/packages/ui/components/custom/file-button.tsx +++ b/packages/ui/components/custom/file-button.tsx @@ -1,3 +1,4 @@ +"use client"; import React, { useRef, ChangeEvent } from "react"; import { Button, ButtonProps } from "../button"; From fabf278babc5c90b19aed34d27bee72b034d0a03 Mon Sep 17 00:00:00 2001 From: CI Date: Tue, 19 Nov 2024 19:20:21 +0000 Subject: [PATCH 20/68] chore(release): released version console-api/v2.32.0-beta.1 --- apps/api/CHANGELOG.md | 7 +++++++ apps/api/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/api/CHANGELOG.md b/apps/api/CHANGELOG.md index 99a34a2e3..8c466b6eb 100644 --- a/apps/api/CHANGELOG.md +++ b/apps/api/CHANGELOG.md @@ -1,5 +1,12 @@ +## [2.32.0-beta.1](https://github.com/akash-network/console/compare/console-api/v2.32.0-beta.0...console-api/v2.32.0-beta.1) (2024-11-19) + + +### Features + +* **billing:** enable promo codes on checkout via env var ([18f24f6](https://github.com/akash-network/console/commit/18f24f61d52d19364588545323ab621dcdd3b440)) + ## [2.32.0-beta.0](https://github.com/akash-network/console/compare/console-api/v2.31.0...console-api/v2.32.0-beta.0) (2024-11-19) diff --git a/apps/api/package.json b/apps/api/package.json index 5287a7983..dc62235e6 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-api", - "version": "2.32.0-beta.0", + "version": "2.32.0-beta.1", "description": "Api providing data to the deploy tool", "repository": { "type": "git", From f691d6c0591029cc9cab902ce3b7698572242f7d Mon Sep 17 00:00:00 2001 From: CI Date: Tue, 19 Nov 2024 19:26:26 +0000 Subject: [PATCH 21/68] chore(release): released version console-web/v2.23.0-beta.2 --- apps/deploy-web/CHANGELOG.md | 7 +++++++ apps/deploy-web/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/deploy-web/CHANGELOG.md b/apps/deploy-web/CHANGELOG.md index ac3d78a6d..981db4ebc 100644 --- a/apps/deploy-web/CHANGELOG.md +++ b/apps/deploy-web/CHANGELOG.md @@ -1,5 +1,12 @@ +## [2.23.0-beta.2](https://github.com/akash-network/console/compare/console-web/v2.23.0-beta.1...console-web/v2.23.0-beta.2) (2024-11-19) + + +### Bug Fixes + +* **deployment:** managed wallet user template ([ab83f2f](https://github.com/akash-network/console/commit/ab83f2f699e84b3a4f90739d2d003a9f8e9d27aa)), closes [#483](https://github.com/akash-network/console/issues/483) + ## [2.23.0-beta.1](https://github.com/akash-network/console/compare/console-web/v2.23.0-beta.0...console-web/v2.23.0-beta.1) (2024-11-19) diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index 65f157d1f..c281b4db7 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-web", - "version": "2.23.0-beta.1", + "version": "2.23.0-beta.2", "private": true, "description": "Web UI to deploy on the Akash Network and view statistic about network usage.", "license": "Apache-2.0", From 95a6e2b11353b1c07a23f763ef9c9216855025b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Kozma?= Date: Tue, 19 Nov 2024 20:51:46 +0100 Subject: [PATCH 22/68] feat(sdl): sdl builder private containers (#479) --- .../components/sdl/ImageCredentialsHost.tsx | 59 +++++++++ .../sdl/ImageCredentialsPassword.tsx | 71 +++++++++++ .../sdl/ImageCredentialsUsername.tsx | 45 +++++++ .../src/components/sdl/ImageInput.tsx | 114 ++++++++++++++++++ .../src/components/sdl/ImageRegistryLogo.tsx | 25 ++++ .../src/components/sdl/ImportSdlModal.tsx | 8 +- .../sdl/SimpleServiceFormControl.tsx | 102 ++++++---------- apps/deploy-web/src/types/sdlBuilder.ts | 8 ++ apps/deploy-web/src/utils/sdl/sdlGenerator.ts | 2 + apps/deploy-web/src/utils/sdl/sdlImport.ts | 4 +- 10 files changed, 373 insertions(+), 65 deletions(-) create mode 100644 apps/deploy-web/src/components/sdl/ImageCredentialsHost.tsx create mode 100644 apps/deploy-web/src/components/sdl/ImageCredentialsPassword.tsx create mode 100644 apps/deploy-web/src/components/sdl/ImageCredentialsUsername.tsx create mode 100644 apps/deploy-web/src/components/sdl/ImageInput.tsx create mode 100644 apps/deploy-web/src/components/sdl/ImageRegistryLogo.tsx diff --git a/apps/deploy-web/src/components/sdl/ImageCredentialsHost.tsx b/apps/deploy-web/src/components/sdl/ImageCredentialsHost.tsx new file mode 100644 index 000000000..dcf93ec1a --- /dev/null +++ b/apps/deploy-web/src/components/sdl/ImageCredentialsHost.tsx @@ -0,0 +1,59 @@ +"use client"; +import { Control } from "react-hook-form"; +import { + FormField, + FormItem, + FormLabel, + FormMessage, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@akashnetwork/ui/components"; + +import { SdlBuilderFormValuesType } from "@src/types"; + +type Props = { + serviceIndex: number; + control: Control; +}; + +const supportedHosts = [ + { id: 'docker.io', label: 'Docker Hub' }, + { id: 'ghcr.io', label: 'GitHub Container Registry' } +]; + +export const ImageCredentialsHost: React.FunctionComponent = ({ + serviceIndex, + control, +}) => { + return ( + ( + + Host + + + + + )} + /> + ); +}; diff --git a/apps/deploy-web/src/components/sdl/ImageCredentialsPassword.tsx b/apps/deploy-web/src/components/sdl/ImageCredentialsPassword.tsx new file mode 100644 index 000000000..f1265f6a6 --- /dev/null +++ b/apps/deploy-web/src/components/sdl/ImageCredentialsPassword.tsx @@ -0,0 +1,71 @@ +"use client"; +import { useCallback, useMemo, useState } from "react"; +import { Control } from "react-hook-form"; +import { + buttonVariants, + FormField, + FormItem, + FormMessage, + Input } from "@akashnetwork/ui/components"; +import { cn } from "@akashnetwork/ui/utils"; +import { EyeClosed, EyeSolid } from "iconoir-react"; + +import { SdlBuilderFormValuesType } from "@src/types"; + +type Props = { + serviceIndex: number; + control: Control; + label: string; +}; + +export const ImageCredentialsPassword: React.FunctionComponent = ({ + serviceIndex, + control, + label, +}) => { + const [type, setType] = useState('password'); + const toggleType = useCallback(() => { + setType(type === 'password' ? 'text' : 'password'); + }, [type]); + const isClosed = useMemo(() => type === 'password', [type]); + + return ( + ( + + + {label} +

    + } + value={field.value} + error={!!fieldState.error} + onChange={event => field.onChange(event.target.value || "")} + endIcon={ + + {isClosed ? () : ()} + + + } + data-testid="credentials-password-input" + /> + + + + )} + /> + ); +}; diff --git a/apps/deploy-web/src/components/sdl/ImageCredentialsUsername.tsx b/apps/deploy-web/src/components/sdl/ImageCredentialsUsername.tsx new file mode 100644 index 000000000..38e7e3eee --- /dev/null +++ b/apps/deploy-web/src/components/sdl/ImageCredentialsUsername.tsx @@ -0,0 +1,45 @@ +"use client"; +import { Control } from "react-hook-form"; +import { + FormField, + FormItem, + FormMessage, + Input, +} from "@akashnetwork/ui/components"; + +import { SdlBuilderFormValuesType } from "@src/types"; + +type Props = { + serviceIndex: number; + control: Control; +}; + +export const ImageCredentialsUsername: React.FunctionComponent = ({ + serviceIndex, + control, +}) => { + return ( + ( + + + Username +
    + } + value={field.value} + error={!!fieldState.error} + onChange={event => field.onChange(event.target.value || "")} + data-testid="credentials-username-input" + /> + + + + )} + /> + ); +}; diff --git a/apps/deploy-web/src/components/sdl/ImageInput.tsx b/apps/deploy-web/src/components/sdl/ImageInput.tsx new file mode 100644 index 000000000..fd0216df3 --- /dev/null +++ b/apps/deploy-web/src/components/sdl/ImageInput.tsx @@ -0,0 +1,114 @@ +"use client"; +import { Control, UseFormSetValue } from "react-hook-form"; +import { + buttonVariants, + Checkbox, + CustomTooltip, + FormField, + FormItem, + FormMessage, + Input +} from "@akashnetwork/ui/components"; +import { cn } from "@akashnetwork/ui/utils"; +import { InfoCircle, OpenInWindow } from "iconoir-react"; +import Link from "next/link"; + +import { SdlBuilderFormValuesType, ServiceType } from "@src/types"; +import { ImageRegistryLogo } from "./ImageRegistryLogo"; + +type Props = { + serviceIndex: number; + control: Control; + credentials?: ServiceType['credentials']; + setValue: UseFormSetValue; +}; + +const defaultCredentials = { + host: 'docker.io' as 'docker.io' | 'ghcr.io', + username: '', + password: '', +}; + +export const ImageInput: React.FunctionComponent = ({ + serviceIndex, + control, + credentials, + setValue, +}) => { + return ( + ( + + + Docker Image / OS + + Docker image of the container. +
    +
    + Best practices: avoid using :latest image tags as Akash Providers heavily cache images. + + } + > + +
    + ( + <> + { + field.onChange(checked); + setValue(`services.${serviceIndex}.credentials`, checked ? defaultCredentials : undefined); + }} + className="ml-4" + /> + + + )} + /> +
    + } + placeholder="Example: mydockerimage:1.01" + value={field.value} + error={!!fieldState.error} + onChange={event => field.onChange((event.target.value || "").toLowerCase())} + startIconClassName="pl-2" + startIcon={} + endIcon={ + + + + } + data-testid="image-name-input" + /> + + + )} + /> + ); +}; diff --git a/apps/deploy-web/src/components/sdl/ImageRegistryLogo.tsx b/apps/deploy-web/src/components/sdl/ImageRegistryLogo.tsx new file mode 100644 index 000000000..a146dc2c7 --- /dev/null +++ b/apps/deploy-web/src/components/sdl/ImageRegistryLogo.tsx @@ -0,0 +1,25 @@ +"use client"; +import Image from "next/legacy/image"; + +type Props = { + host?: 'docker.io' | 'ghcr.io'; +}; + +const images = { + 'docker.io': { + filename: 'docker', + height: 18 + }, + 'ghcr.io': { + filename: 'github', + height: 24 + } +}; + +export const ImageRegistryLogo: React.FunctionComponent = ({ + host = 'docker.io', +}) => { + return ( + Docker Logo + ); +}; diff --git a/apps/deploy-web/src/components/sdl/ImportSdlModal.tsx b/apps/deploy-web/src/components/sdl/ImportSdlModal.tsx index 2d97c7276..443d5e3ca 100644 --- a/apps/deploy-web/src/components/sdl/ImportSdlModal.tsx +++ b/apps/deploy-web/src/components/sdl/ImportSdlModal.tsx @@ -1,9 +1,10 @@ "use client"; -import { ReactNode, useEffect, useState } from "react"; +import { ReactNode, useCallback, useEffect, useState } from "react"; import { UseFormSetValue } from "react-hook-form"; import { Alert, Popup, Snackbar } from "@akashnetwork/ui/components"; import Editor from "@monaco-editor/react"; import { ArrowDown } from "iconoir-react"; +import { editor } from 'monaco-editor'; import { useTheme } from "next-themes"; import { event } from "nextjs-google-analytics"; import { useSnackbar } from "notistack"; @@ -24,6 +25,9 @@ export const ImportSdlModal: React.FunctionComponent = ({ onClose, setVal const [parsingError, setParsingError] = useState(null); const { enqueueSnackbar } = useSnackbar(); const { resolvedTheme } = useTheme(); + const onEditorMount = useCallback((editorInstance: editor.IStandaloneCodeEditor) => { + editorInstance.focus(); + }, []); useEffect(() => { const timer = Timer(500); @@ -113,7 +117,7 @@ export const ImportSdlModal: React.FunctionComponent = ({ onClose, setVal Paste your sdl here to import
    - setSdl(value)} theme={resolvedTheme === "dark" ? "vs-dark" : "light"} /> + setSdl(value)} theme={resolvedTheme === "dark" ? "vs-dark" : "light"} onMount={onEditorMount} />
    {parsingError && ( diff --git a/apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx b/apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx index 26958ef05..a780bb501 100644 --- a/apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx +++ b/apps/deploy-web/src/components/sdl/SimpleServiceFormControl.tsx @@ -3,7 +3,6 @@ import { Dispatch, SetStateAction, useState } from "react"; import { Control, UseFormSetValue, UseFormTrigger } from "react-hook-form"; import { Button, - buttonVariants, Card, CardContent, CheckboxWithLabel, @@ -14,8 +13,6 @@ import { FormField, FormInput, FormItem, - FormMessage, - Input, Select, SelectContent, SelectGroup, @@ -26,9 +23,8 @@ import { import { cn } from "@akashnetwork/ui/utils"; import { useTheme as useMuiTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; -import { BinMinusIn, InfoCircle, NavArrowDown, OpenInWindow } from "iconoir-react"; +import { BinMinusIn, InfoCircle, NavArrowDown } from "iconoir-react"; import Image from "next/legacy/image"; -import Link from "next/link"; import { SSHKeyFormControl } from "@src/components/sdl/SSHKeyFromControl"; import { UAKT_DENOM } from "@src/config/denom.config"; @@ -49,6 +45,10 @@ import { ExposeFormModal } from "./ExposeFormModal"; import { ExposeList } from "./ExposeList"; import { FormPaper } from "./FormPaper"; import { GpuFormControl } from "./GpuFormControl"; +import { ImageCredentialsHost } from "./ImageCredentialsHost"; +import { ImageCredentialsPassword } from "./ImageCredentialsPassword"; +import { ImageCredentialsUsername } from "./ImageCredentialsUsername"; +import { ImageInput } from "./ImageInput"; import { MemoryFormControl } from "./MemoryFormControl"; import { PersistentStorage } from "./PersistentStorage"; import { PlacementFormModal } from "./PlacementFormModal"; @@ -80,7 +80,7 @@ export const SimpleServiceFormControl: React.FunctionComponent = ({ setValue, gpuModels, hasSecretOption, - isGitProviderTemplate + isGitProviderTemplate, }) => { const [isEditingCommands, setIsEditingCommands] = useState(null); const [isEditingEnv, setIsEditingEnv] = useState(null); @@ -94,6 +94,8 @@ export const SimpleServiceFormControl: React.FunctionComponent = ({ const _isEditingCommands = serviceIndex === isEditingCommands; const _isEditingExpose = serviceIndex === isEditingExpose; const _isEditingPlacement = serviceIndex === isEditingPlacement; + const _credentials = _services[serviceIndex]?.credentials; + const _isGhcr = _credentials?.host === 'ghcr.io'; const { imageList, hasComponent, toggleCmp } = useSdlBuilder(); const wallet = useWallet(); const onExpandClick = () => { @@ -219,13 +221,13 @@ export const SimpleServiceFormControl: React.FunctionComponent = ({
    {!isGitProviderTemplate && ( -
    - ( - - {imageList?.length ? ( + (imageList?.length ? ( +
    + ( +
    - ) : ( - - Docker Image / OS - - Docker image of the container. -
    -
    - Best practices: avoid using :latest image tags as Akash Providers heavily cache images. - - } - > - -
    -
    - } - placeholder="Example: mydockerimage:1.01" - value={field.value} - error={!!fieldState.error} - onChange={event => field.onChange((event.target.value || "").toLowerCase())} - startIconClassName="pl-2" - startIcon={Docker Logo} - endIcon={ - - - - } - data-testid="image-name-input" - /> - )} - - -
    + + )} + /> +
    + ) : ( + +
    + +
    + {_services[serviceIndex]?.hasCredentials && ( + <> +
    + +
    +
    +
    + +
    +
    + +
    +
    + )} - /> -
    + + )) )}
    diff --git a/apps/deploy-web/src/types/sdlBuilder.ts b/apps/deploy-web/src/types/sdlBuilder.ts index 270d64792..a1c9d7024 100644 --- a/apps/deploy-web/src/types/sdlBuilder.ts +++ b/apps/deploy-web/src/types/sdlBuilder.ts @@ -74,6 +74,12 @@ export const SignedBySchema = z.object({ value: z.string().min(1, { message: "Value is required." }) }); +export const CredentialsSchema = z.object({ + host: z.enum(['docker.io', 'ghcr.io']).default('docker.io'), + username: z.string(), + password: z.string(), +}).optional(); + export const ProfileSchema = z .object({ cpu: z.number({ invalid_type_error: "CPU count is required." }).min(0.1, { message: "CPU count is required." }), @@ -295,6 +301,8 @@ export const ServiceSchema = z .regex(/^[a-z]/, { message: "Invalid starting character. It can only start with a lowercase letter." }) .regex(/[^-]$/, { message: "Invalid ending character. It can only end with a lowercase letter or number" }), image: z.string().min(1, { message: "Docker image name is required." }), + hasCredentials: z.boolean().optional(), + credentials: CredentialsSchema, profile: ProfileSchema, expose: z.array(ExposeSchema), command: CommandSchema.optional(), diff --git a/apps/deploy-web/src/utils/sdl/sdlGenerator.ts b/apps/deploy-web/src/utils/sdl/sdlGenerator.ts index c2326c655..d60e140c3 100644 --- a/apps/deploy-web/src/utils/sdl/sdlGenerator.ts +++ b/apps/deploy-web/src/utils/sdl/sdlGenerator.ts @@ -10,6 +10,8 @@ export const generateSdl = (services: ServiceType[], region?: string) => { sdl.services[service.title] = { image: service.image, + credentials: service.hasCredentials ? service.credentials : undefined, + // Expose expose: service.expose.map(e => { // Port diff --git a/apps/deploy-web/src/utils/sdl/sdlImport.ts b/apps/deploy-web/src/utils/sdl/sdlImport.ts index 490bbe060..cfa8fd67e 100644 --- a/apps/deploy-web/src/utils/sdl/sdlImport.ts +++ b/apps/deploy-web/src/utils/sdl/sdlImport.ts @@ -18,7 +18,9 @@ export const importSimpleSdl = (yamlStr: string) => { const service: Partial = { id: nanoid(), title: svcName, - image: svc.image + image: svc.image, + hasCredentials: !!svc.credentials, + credentials: svc.credentials, }; const compute = yamlJson.profiles.compute[svcName]; From 2ccd2b6b99c7abac5d2b9eac8f410e43b919a99a Mon Sep 17 00:00:00 2001 From: CI Date: Tue, 19 Nov 2024 19:59:41 +0000 Subject: [PATCH 23/68] chore(release): released version console-web/v2.23.0-beta.3 --- apps/deploy-web/CHANGELOG.md | 7 +++++++ apps/deploy-web/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/deploy-web/CHANGELOG.md b/apps/deploy-web/CHANGELOG.md index 981db4ebc..1d6377a0b 100644 --- a/apps/deploy-web/CHANGELOG.md +++ b/apps/deploy-web/CHANGELOG.md @@ -1,5 +1,12 @@ +## [2.23.0-beta.3](https://github.com/akash-network/console/compare/console-web/v2.23.0-beta.2...console-web/v2.23.0-beta.3) (2024-11-19) + + +### Features + +* **sdl:** sdl builder private containers ([95a6e2b](https://github.com/akash-network/console/commit/95a6e2b11353b1c07a23f763ef9c9216855025b1)), closes [#479](https://github.com/akash-network/console/issues/479) + ## [2.23.0-beta.2](https://github.com/akash-network/console/compare/console-web/v2.23.0-beta.1...console-web/v2.23.0-beta.2) (2024-11-19) diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index c281b4db7..f0f9bfa02 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -1,6 +1,6 @@ { "name": "@akashnetwork/console-web", - "version": "2.23.0-beta.2", + "version": "2.23.0-beta.3", "private": true, "description": "Web UI to deploy on the Akash Network and view statistic about network usage.", "license": "Apache-2.0", From e65d61dbdcbaf466a61c4f35b09ca8abab1bad47 Mon Sep 17 00:00:00 2001 From: Sergii Stotskyi Date: Tue, 19 Nov 2024 22:09:18 +0200 Subject: [PATCH 24/68] refactor: splits templates API into list and fetch by id (#481) --------- Co-authored-by: Serhii --- apps/api/src/routes/v1/index.ts | 8 ++- apps/api/src/routes/v1/templates/byId.ts | 55 +++++++++++++++++++ .../{templates.ts => templates/list-full.ts} | 12 ++-- apps/api/src/routes/v1/templates/list.ts | 42 ++++++++++++++ .../services/external/templateReposService.ts | 16 ++++++ 5 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 apps/api/src/routes/v1/templates/byId.ts rename apps/api/src/routes/v1/{templates.ts => templates/list-full.ts} (78%) create mode 100644 apps/api/src/routes/v1/templates/list.ts diff --git a/apps/api/src/routes/v1/index.ts b/apps/api/src/routes/v1/index.ts index e02da0b1d..eed2e40a1 100644 --- a/apps/api/src/routes/v1/index.ts +++ b/apps/api/src/routes/v1/index.ts @@ -12,6 +12,9 @@ import proposals from "./proposals/list"; import providerByAddress from "./providers/byAddress"; import providerDeployments from "./providers/deployments"; import providerList from "./providers/list"; +import templateById from "./templates/byId"; +import templateList from "./templates/list"; +import templateListFull from "./templates/list-full"; import transactionByHash from "./transactions/byHash"; import transactions from "./transactions/list"; import validatorByAddress from "./validators/byAddress"; @@ -31,7 +34,6 @@ import providerActiveLeasesGraphData from "./providerActiveLeasesGraphData"; import providerAttributesSchema from "./providerAttributesSchema"; import providerGraphData from "./providerGraphData"; import providerRegions from "./providerRegions"; -import templates from "./templates"; import trialProviders from "./trialProviders"; export default [ @@ -53,7 +55,9 @@ export default [ validatorByAddress, proposals, proposalById, - templates, + templateListFull, + templateList, + templateById, networkCapacity, marketData, dashboardData, diff --git a/apps/api/src/routes/v1/templates/byId.ts b/apps/api/src/routes/v1/templates/byId.ts new file mode 100644 index 000000000..79afe58a5 --- /dev/null +++ b/apps/api/src/routes/v1/templates/byId.ts @@ -0,0 +1,55 @@ +import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; + +import { getCachedTemplateById } from "@src/services/external/templateReposService"; + +const route = createRoute({ + method: "get", + path: "/templates/{id}", + tags: ["Other"], + request: { + params: z.object({ + id: z.string().openapi({ + description: "Template ID", + example: "akash-network-cosmos-omnibus-agoric" + }) + }) + }, + responses: { + 200: { + description: "Return a template by id", + content: { + "application/json": { + schema: z.object({ + id: z.string(), + name: z.string(), + path: z.string(), + logoUrl: z.string().nullable(), + summary: z.string(), + readme: z.string().nullable(), + deploy: z.string(), + persistentStorageEnabled: z.boolean(), + guide: z.string().nullable(), + githubUrl: z.string(), + config: z.object({ + ssh: z.boolean().optional() + }) + }) + } + } + }, + 404: { + description: "Template not found" + } + } +}); + +export default new OpenAPIHono().openapi(route, async c => { + const templateId = c.req.valid("param").id; + const template = await getCachedTemplateById(templateId); + + if (!template) { + return c.text("Template not found", 404); + } + + return c.json(template); +}); diff --git a/apps/api/src/routes/v1/templates.ts b/apps/api/src/routes/v1/templates/list-full.ts similarity index 78% rename from apps/api/src/routes/v1/templates.ts rename to apps/api/src/routes/v1/templates/list-full.ts index 9ce8b335e..37a1abdd0 100644 --- a/apps/api/src/routes/v1/templates.ts +++ b/apps/api/src/routes/v1/templates/list-full.ts @@ -1,7 +1,6 @@ import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; -import { cacheKeys, cacheResponse } from "@src/caching/helpers"; -import { getTemplateGallery } from "@src/services/external/templateReposService"; +import { getCachedTemplatesGallery } from "@src/services/external/templateReposService"; const route = createRoute({ method: "get", @@ -9,7 +8,7 @@ const route = createRoute({ tags: ["Other"], responses: { 200: { - description: "Returns a list of deployment templates grouped by cateogories", + description: "Returns a list of deployment templates grouped by categories", content: { "application/json": { schema: z.array( @@ -40,7 +39,10 @@ const route = createRoute({ } }); +/** + * @deprecated should stay for some time in order to let UI to migrate to shorten list version. + */ export default new OpenAPIHono().openapi(route, async c => { - const response = await cacheResponse(60 * 5, cacheKeys.getTemplates, async () => await getTemplateGallery(), true); - return c.json(response); + const templatesPerCategory = await getCachedTemplatesGallery(); + return c.json(templatesPerCategory); }); diff --git a/apps/api/src/routes/v1/templates/list.ts b/apps/api/src/routes/v1/templates/list.ts new file mode 100644 index 000000000..4f853b3a5 --- /dev/null +++ b/apps/api/src/routes/v1/templates/list.ts @@ -0,0 +1,42 @@ +import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; + +import { getCachedTemplatesGallery } from "@src/services/external/templateReposService"; + +const responseSchema = z.array( + z.object({ + title: z.string(), + templates: z.array( + z.object({ + id: z.string(), + name: z.string(), + logoUrl: z.string().nullable(), + summary: z.string(), + }) + ) + }) +); +const route = createRoute({ + method: "get", + path: "/templates-list", + tags: ["Other"], + responses: { + 200: { + description: "Returns a list of deployment templates grouped by categories", + content: { + "application/json": { + schema: responseSchema + } + } + } + } +}); + +export default new OpenAPIHono().openapi(route, async c => { + const templatesPerCategory = await getCachedTemplatesGallery(); + // TODO: remove manual response filtering when https://github.com/honojs/middleware/issues/181 is done + const filteredTemplatesPerCategory = await responseSchema.safeParseAsync(templatesPerCategory); + const response = filteredTemplatesPerCategory.success + ? filteredTemplatesPerCategory.data + : templatesPerCategory; + return c.json(response); +}); diff --git a/apps/api/src/services/external/templateReposService.ts b/apps/api/src/services/external/templateReposService.ts index eb08afa76..096573809 100644 --- a/apps/api/src/services/external/templateReposService.ts +++ b/apps/api/src/services/external/templateReposService.ts @@ -4,6 +4,7 @@ import { markdownToTxt } from "markdown-to-txt"; import fetch from "node-fetch"; import path from "path"; +import { cacheKeys, cacheResponse } from "@src/caching/helpers"; import { GithubChainRegistryChainResponse } from "@src/types"; import { GithubDirectoryItem } from "@src/types/github"; import { dataFolderPath } from "@src/utils/constants"; @@ -129,6 +130,21 @@ export const getTemplateGallery = async () => { } }; +export const getCachedTemplatesGallery = (): Promise => cacheResponse(60 * 5, cacheKeys.getTemplates, () => getTemplateGallery(), true); + +export const getTemplateById = async (id: Required