diff --git a/apps/backend/README.md b/apps/backend/README.md deleted file mode 100644 index 7fb3c8e..0000000 --- a/apps/backend/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# backend - -This backend is a [node.js](https://nodejs.org/) [express.js](https://expressjs.com/) [apollo server](https://www.apollographql.com/docs/apollo-server/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces). - -## Getting Started - -### Install dependencies - -In the root of the project, run: - -```zsh -yarn -``` - -### Build backend - -```zsh -yarn build --ignore frontend -``` - -### Environment variables - -#### Local - -Copy the `environments/local.toml.example` file to `environments/local.toml`: - -```zsh -cp environments/local.toml.example environments/local.toml -``` - -#### Staging environment variables - -In the deployment repository, update staging [staging/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/staging/configmaps/config/prod.toml) - -#### Production environment variables - -In the deployment repository, update production [production/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/production/configmaps/config/prod.toml) - -### Run development server - -```zsh -yarn start -``` - -## Deployment - -Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments): - -```zsh -git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git -``` - -### Staging - -```zsh -echo trigger >> .gitea/workflows/triggers/staging-deploy -git commit -a -m "Deploy v0.0.8" # replace with version number -git push -``` - -### Production - -```zsh -echo trigger >> .gitea/workflows/triggers/production-deploy -git commit -a -m "Deploy v0.0.8" # replace with version number -git push -``` - -### Deployment status - -Dumb for now - -- [Staging](https://snowballtools-base-api.staging.apps.snowballtools.com/staging/version) -- [Production](https://snowballtools-base-api.apps.snowballtools.com/staging/version) - -Update version number manually in [routes/staging.ts](/packages/backend/src/routes/staging.ts) diff --git a/apps/backend/biome.json b/apps/backend/biome.json deleted file mode 100644 index dd2cde3..0000000 --- a/apps/backend/biome.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 80 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "suspicious": { - "noExplicitAny": "off" - }, - "style": { - "noNonNullAssertion": "off" - } - } - }, - "javascript": { - "formatter": { - "enabled": true, - "quoteStyle": "single", - "trailingCommas": "none", - "semicolons": "asNeeded" - } - }, - "files": { - "ignore": ["dist/**/*", "node_modules/**/*", ".turbo/**/*"] - } -} diff --git a/apps/backend/environments/local.toml b/apps/backend/environments/local.toml deleted file mode 100644 index 233d495..0000000 --- a/apps/backend/environments/local.toml +++ /dev/null @@ -1,43 +0,0 @@ -[server] - host = "127.0.0.1" - port = 8000 - gqlPath = "/graphql" - [server.session] - secret = "" - # Frontend webapp URL origin - appOriginUrl = "http://localhost:3000" - # Set to true if server running behind proxy - trustProxy = false - # Backend URL hostname - domain = "localhost" - -[database] - dbPath = "db/snowball" - -[gitHub] - webhookUrl = "" - [gitHub.oAuth] - clientId = "" - clientSecret = "" - -[registryConfig] - fetchDeploymentRecordDelay = 5000 - checkAuctionStatusDelay = 5000 - restEndpoint = "http://localhost:1317" - gqlEndpoint = "http://localhost:9473/api" - chainId = "laconic_9000-1" - privateKey = "" - bondId = "" - authority = "" - [registryConfig.fee] - gas = "" - fees = "" - gasPrice = "1alnt" - -# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions -[auction] - commitFee = "100000" - commitsDuration = "120s" - revealFee = "100000" - revealsDuration = "120s" - denom = "alnt" diff --git a/apps/backend/environments/local.toml.example b/apps/backend/environments/local.toml.example deleted file mode 100644 index 233d495..0000000 --- a/apps/backend/environments/local.toml.example +++ /dev/null @@ -1,43 +0,0 @@ -[server] - host = "127.0.0.1" - port = 8000 - gqlPath = "/graphql" - [server.session] - secret = "" - # Frontend webapp URL origin - appOriginUrl = "http://localhost:3000" - # Set to true if server running behind proxy - trustProxy = false - # Backend URL hostname - domain = "localhost" - -[database] - dbPath = "db/snowball" - -[gitHub] - webhookUrl = "" - [gitHub.oAuth] - clientId = "" - clientSecret = "" - -[registryConfig] - fetchDeploymentRecordDelay = 5000 - checkAuctionStatusDelay = 5000 - restEndpoint = "http://localhost:1317" - gqlEndpoint = "http://localhost:9473/api" - chainId = "laconic_9000-1" - privateKey = "" - bondId = "" - authority = "" - [registryConfig.fee] - gas = "" - fees = "" - gasPrice = "1alnt" - -# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions -[auction] - commitFee = "100000" - commitsDuration = "120s" - revealFee = "100000" - revealsDuration = "120s" - denom = "alnt" diff --git a/apps/backend/package.json b/apps/backend/package.json deleted file mode 100644 index fbf128c..0000000 --- a/apps/backend/package.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "name": "@qwrk/backend", - "license": "UNLICENSED", - "version": "1.0.0", - "private": true, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "dependencies": { - "@cerc-io/registry-sdk": "^0.2.11", - "@cosmjs/stargate": "^0.33.0", - "@graphql-tools/schema": "^10.0.2", - "@graphql-tools/utils": "^10.0.12", - "@octokit/oauth-app": "^6.1.0", - "@turnkey/sdk-server": "^0.1.0", - "@types/debug": "^4.1.5", - "@types/node": "^20.11.0", - "@types/semver": "^7.5.8", - "apollo-server-core": "^3.13.0", - "apollo-server-express": "^3.13.0", - "cookie-session": "^2.1.0", - "cors": "^2.8.5", - "debug": "^4.3.1", - "express": "^4.18.2", - "express-async-errors": "^3.1.1", - "express-session": "^1.18.0", - "fs-extra": "^11.2.0", - "graphql": "^16.8.1", - "luxon": "^3.5.0", - "nanoid": "3", - "nanoid-dictionary": "^5.0.0-beta.1", - "octokit": "^3.1.2", - "openpgp": "^6.0.1", - "reflect-metadata": "^0.2.1", - "semver": "^7.6.0", - "siwe": "^3.0.0", - "toml": "^3.0.0", - "ts-node": "^10.9.2", - "typeorm": "^0.3.19", - "typescript": "^5.3.3" - }, - "scripts": { - "start": "DEBUG=snowball:* node --enable-source-maps ./dist/index.js", - "start:dev": "DEBUG=snowball:* ts-node ./src/index.ts", - "copy-assets": "copyfiles -u 1 src/**/*.gql dist/", - "clean": "rm -rf ./dist", - "build": "pnpm clean && tsc && pnpm copy-assets", - "format": "biome format .", - "format:check": "biome format --check .", - "lint": "biome check .", - "test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts", - "test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts", - "test:registry:publish-deployment-removal-records": "DEBUG=snowball:* ts-node ./test/publish-deployment-removal-records.ts", - "test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts", - "test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts" - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@types/cookie-session": "^2.0.49", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/express-session": "^1.17.10", - "@types/fs-extra": "^11.0.4", - "better-sqlite3": "^9.2.2", - "copyfiles": "^2.4.1", - "prettier": "^3.1.1", - "workspace": "^0.0.1-preview.1" - } -} diff --git a/apps/backend/src/config.ts b/apps/backend/src/config.ts deleted file mode 100644 index 53a071a..0000000 --- a/apps/backend/src/config.ts +++ /dev/null @@ -1,66 +0,0 @@ -export interface SessionConfig { - secret: string - appOriginUrl: string - trustProxy: boolean - domain: string -} - -export interface ServerConfig { - host: string - port: number - gqlPath?: string - sessionSecret: string - appOriginUrl: string - isProduction: boolean - session: SessionConfig -} - -export interface DatabaseConfig { - dbPath: string -} - -export interface GitHubConfig { - webhookUrl: string - oAuth: { - clientId: string - clientSecret: string - } -} - -export interface RegistryConfig { - restEndpoint: string - gqlEndpoint: string - chainId: string - privateKey: string - bondId: string - fetchDeploymentRecordDelay: number - checkAuctionStatusDelay: number - authority: string - fee: { - gas: string - fees: string - gasPrice: string - } -} - -export interface AuctionConfig { - commitFee: string - commitsDuration: string - revealFee: string - revealsDuration: string - denom: string -} - -export interface Config { - server: ServerConfig - database: DatabaseConfig - gitHub: GitHubConfig - registryConfig: RegistryConfig - auction: AuctionConfig - turnkey: { - apiBaseUrl: string - apiPublicKey: string - apiPrivateKey: string - defaultOrganizationId: string - } -} diff --git a/apps/backend/src/constants.ts b/apps/backend/src/constants.ts deleted file mode 100644 index be2a86e..0000000 --- a/apps/backend/src/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -import process from 'node:process' - -export const DEFAULT_CONFIG_FILE_PATH = - process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || - 'apps/backend/environments/local.toml' - -export const DEFAULT_GQL_PATH = '/graphql' diff --git a/apps/backend/src/database.ts b/apps/backend/src/database.ts deleted file mode 100644 index 6528bfe..0000000 --- a/apps/backend/src/database.ts +++ /dev/null @@ -1,694 +0,0 @@ -import assert from 'node:assert' -import path from 'node:path' -import debug from 'debug' -import { customAlphabet } from 'nanoid' -import { lowercase, numbers } from 'nanoid-dictionary' -import { - DataSource, - type DeepPartial, - type FindManyOptions, - type FindOneOptions, - type FindOptionsWhere, - IsNull, - Not -} from 'typeorm' - -import type { DatabaseConfig } from './config' -import { Deployer } from './entity/Deployer' -import { Deployment, DeploymentStatus } from './entity/Deployment' -import { Domain } from './entity/Domain' -import { EnvironmentVariable } from './entity/EnvironmentVariable' -import { Organization } from './entity/Organization' -import { Project } from './entity/Project' -import { ProjectMember } from './entity/ProjectMember' -import { User } from './entity/User' -import { UserOrganization } from './entity/UserOrganization' -import type { DNSRecordAttributes } from './types' -import { getEntities, loadAndSaveData } from './utils' - -const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json' - -const log = debug('snowball:database') - -const nanoid = customAlphabet(lowercase + numbers, 8) - -// TODO: Fix order of methods -export class Database { - private dataSource: DataSource - - constructor({ dbPath }: DatabaseConfig) { - this.dataSource = new DataSource({ - type: 'better-sqlite3', - database: dbPath, - entities: [path.join(__dirname, '/entity/*')], - synchronize: true, - logging: false - }) - } - - async init(): Promise { - await this.dataSource.initialize() - log('database initialized') - - let organizations = await this.getOrganizations({}) - - // Load an organization if none exist - if (!organizations.length) { - const orgEntities = await getEntities( - path.resolve(__dirname, ORGANIZATION_DATA_PATH) - ) - organizations = await loadAndSaveData(Organization, this.dataSource, [ - orgEntities[0] - ]) - } - - // Hotfix for updating old DB data - if (organizations[0].slug === 'snowball-tools-1') { - const [orgEntity] = await getEntities( - path.resolve(__dirname, ORGANIZATION_DATA_PATH) - ) - - await this.updateOrganization(organizations[0].id, { - slug: orgEntity.slug as string, - name: orgEntity.name as string - }) - } - } - - async getUser(options: FindOneOptions): Promise { - const userRepository = this.dataSource.getRepository(User) - const user = await userRepository.findOne(options) - - return user - } - - async addUser(data: DeepPartial): Promise { - const userRepository = this.dataSource.getRepository(User) - const user = await userRepository.save(data) - - return user - } - - async updateUser(user: User, data: DeepPartial): Promise { - const userRepository = this.dataSource.getRepository(User) - const updateResult = await userRepository.update({ id: user.id }, data) - assert(updateResult.affected) - - return updateResult.affected > 0 - } - - async getOrganizations( - options: FindManyOptions - ): Promise { - const organizationRepository = this.dataSource.getRepository(Organization) - const organizations = await organizationRepository.find(options) - - return organizations - } - - async getOrganization( - options: FindOneOptions - ): Promise { - const organizationRepository = this.dataSource.getRepository(Organization) - const organization = await organizationRepository.findOne(options) - - return organization - } - - async getOrganizationsByUserId(userId: string): Promise { - const organizationRepository = this.dataSource.getRepository(Organization) - - const userOrgs = await organizationRepository.find({ - where: { - userOrganizations: { - member: { - id: userId - } - } - } - }) - - return userOrgs - } - - async addUserOrganization( - data: DeepPartial - ): Promise { - const userOrganizationRepository = - this.dataSource.getRepository(UserOrganization) - const newUserOrganization = await userOrganizationRepository.save(data) - - return newUserOrganization - } - - async updateOrganization( - organizationId: string, - data: DeepPartial - ): Promise { - const organizationRepository = this.dataSource.getRepository(Organization) - const updateResult = await organizationRepository.update( - { id: organizationId }, - data - ) - assert(updateResult.affected) - - return updateResult.affected > 0 - } - - async getProjects(options: FindManyOptions): Promise { - const projectRepository = this.dataSource.getRepository(Project) - const projects = await projectRepository.find(options) - - return projects - } - - async getProjectById(projectId: string): Promise { - const projectRepository = this.dataSource.getRepository(Project) - - const project = await projectRepository - .createQueryBuilder('project') - .leftJoinAndSelect( - 'project.deployments', - 'deployments', - 'deployments.isCurrent = true AND deployments.isCanonical = true' - ) - .leftJoinAndSelect('deployments.createdBy', 'user') - .leftJoinAndSelect('deployments.deployer', 'deployer') - .leftJoinAndSelect('project.owner', 'owner') - .leftJoinAndSelect('project.deployers', 'deployers') - .leftJoinAndSelect('project.organization', 'organization') - .where('project.id = :projectId', { - projectId - }) - .getOne() - - return project - } - - async allProjectsWithoutDeployments(): Promise { - const allProjects = await this.getProjects({ - where: { - auctionId: Not(IsNull()) - }, - relations: ['deployments'], - withDeleted: true - }) - - const projects = allProjects.filter((project) => { - if (project.deletedAt !== null) return false - - return project.deployments.length === 0 - }) - - return projects - } - - async getProjectsInOrganization( - userId: string, - organizationSlug: string - ): Promise { - const projectRepository = this.dataSource.getRepository(Project) - - const projects = await projectRepository - .createQueryBuilder('project') - .leftJoinAndSelect( - 'project.deployments', - 'deployments', - 'deployments.isCurrent = true AND deployments.isCanonical = true' - ) - .leftJoin('project.projectMembers', 'projectMembers') - .leftJoin('project.organization', 'organization') - .where( - '(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', - { - userId, - organizationSlug - } - ) - .getMany() - - return projects - } - - /** - * Get deployments with specified filter - */ - async getDeployments( - options: FindManyOptions - ): Promise { - const deploymentRepository = this.dataSource.getRepository(Deployment) - const deployments = await deploymentRepository.find(options) - - return deployments - } - - async getDeploymentsByProjectId(projectId: string): Promise { - return this.getDeployments({ - relations: { - project: true, - createdBy: true, - deployer: true - }, - where: { - project: { - id: projectId - } - }, - order: { - createdAt: 'DESC' - } - }) - } - - async getNonCanonicalDeploymentsByProjectId( - projectId: string - ): Promise { - return this.getDeployments({ - relations: { - project: true, - createdBy: true, - deployer: true - }, - where: { - project: { - id: projectId - }, - isCanonical: false - }, - order: { - createdAt: 'DESC' - } - }) - } - - async getDeployment( - options: FindOneOptions - ): Promise { - const deploymentRepository = this.dataSource.getRepository(Deployment) - const deployment = await deploymentRepository.findOne(options) - - return deployment - } - - async getDomains(options: FindManyOptions): Promise { - const domainRepository = this.dataSource.getRepository(Domain) - const domains = await domainRepository.find(options) - - return domains - } - - async addDeployment(data: DeepPartial): Promise { - const deploymentRepository = this.dataSource.getRepository(Deployment) - - const id = nanoid() - - const updatedData = { - ...data, - id - } - const deployment = await deploymentRepository.save(updatedData) - - return deployment - } - - async getProjectMembersByProjectId( - projectId: string - ): Promise { - const projectMemberRepository = this.dataSource.getRepository(ProjectMember) - - const projectMembers = await projectMemberRepository.find({ - relations: { - project: true, - member: true - }, - where: { - project: { - id: projectId - } - } - }) - - return projectMembers - } - - async getEnvironmentVariablesByProjectId( - projectId: string, - filter?: FindOptionsWhere - ): Promise { - const environmentVariableRepository = - this.dataSource.getRepository(EnvironmentVariable) - - const environmentVariables = await environmentVariableRepository.find({ - where: { - project: { - id: projectId - }, - ...filter - } - }) - - return environmentVariables - } - - async removeProjectMemberById(projectMemberId: string): Promise { - const projectMemberRepository = this.dataSource.getRepository(ProjectMember) - - const deleteResult = await projectMemberRepository.delete({ - id: projectMemberId - }) - - if (deleteResult.affected) { - return deleteResult.affected > 0 - } - - return false - } - - async updateProjectMemberById( - projectMemberId: string, - data: DeepPartial - ): Promise { - const projectMemberRepository = this.dataSource.getRepository(ProjectMember) - const updateResult = await projectMemberRepository.update( - { id: projectMemberId }, - data - ) - - return Boolean(updateResult.affected) - } - - async addProjectMember( - data: DeepPartial - ): Promise { - const projectMemberRepository = this.dataSource.getRepository(ProjectMember) - const newProjectMember = await projectMemberRepository.save(data) - - return newProjectMember - } - - async addEnvironmentVariables( - data: DeepPartial[] - ): Promise { - const environmentVariableRepository = - this.dataSource.getRepository(EnvironmentVariable) - const savedEnvironmentVariables = - await environmentVariableRepository.save(data) - - return savedEnvironmentVariables - } - - async updateEnvironmentVariable( - environmentVariableId: string, - data: DeepPartial - ): Promise { - const environmentVariableRepository = - this.dataSource.getRepository(EnvironmentVariable) - const updateResult = await environmentVariableRepository.update( - { id: environmentVariableId }, - data - ) - - return Boolean(updateResult.affected) - } - - async deleteEnvironmentVariable( - environmentVariableId: string - ): Promise { - const environmentVariableRepository = - this.dataSource.getRepository(EnvironmentVariable) - const deleteResult = await environmentVariableRepository.delete({ - id: environmentVariableId - }) - - if (deleteResult.affected) { - return deleteResult.affected > 0 - } - - return false - } - - async getProjectMemberById(projectMemberId: string): Promise { - const projectMemberRepository = this.dataSource.getRepository(ProjectMember) - - const projectMemberWithProject = await projectMemberRepository.find({ - relations: { - project: { - owner: true - }, - member: true - }, - where: { - id: projectMemberId - } - }) - - if (projectMemberWithProject.length === 0) { - throw new Error('Member does not exist') - } - - return projectMemberWithProject[0] - } - - async getProjectsBySearchText( - userId: string, - searchText: string - ): Promise { - const projectRepository = this.dataSource.getRepository(Project) - - const projects = await projectRepository - .createQueryBuilder('project') - .leftJoinAndSelect('project.organization', 'organization') - .leftJoin('project.projectMembers', 'projectMembers') - .where( - '(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', - { - userId, - searchText: `%${searchText}%` - } - ) - .getMany() - - return projects - } - - async updateDeploymentById( - deploymentId: string, - data: DeepPartial - ): Promise { - return this.updateDeployment({ id: deploymentId }, data) - } - - async updateDeployment( - criteria: FindOptionsWhere, - data: DeepPartial - ): Promise { - const deploymentRepository = this.dataSource.getRepository(Deployment) - const updateResult = await deploymentRepository.update(criteria, data) - - return Boolean(updateResult.affected) - } - - async updateDeploymentsByProjectIds( - projectIds: string[], - data: DeepPartial - ): Promise { - const deploymentRepository = this.dataSource.getRepository(Deployment) - - const updateResult = await deploymentRepository - .createQueryBuilder() - .update(Deployment) - .set(data) - .where('projectId IN (:...projectIds)', { projectIds }) - .execute() - - return Boolean(updateResult.affected) - } - - async deleteDeploymentById(deploymentId: string): Promise { - const deploymentRepository = this.dataSource.getRepository(Deployment) - const deployment = await deploymentRepository.findOneOrFail({ - where: { - id: deploymentId - } - }) - - const deleteResult = await deploymentRepository.softRemove(deployment) - - return Boolean(deleteResult) - } - - async addProject( - user: User, - organizationId: string, - data: DeepPartial - ): Promise { - const projectRepository = this.dataSource.getRepository(Project) - - // TODO: Check if organization exists - const newProject = projectRepository.create(data) - // TODO: Set default empty array for webhooks in TypeORM - newProject.webhooks = [] - // TODO: Set icon according to framework - newProject.icon = '' - - newProject.owner = user - - newProject.organization = Object.assign(new Organization(), { - id: organizationId - }) - - return projectRepository.save(newProject) - } - - async saveProject(project: Project): Promise { - const projectRepository = this.dataSource.getRepository(Project) - - return projectRepository.save(project) - } - - async updateProjectById( - projectId: string, - data: DeepPartial - ): Promise { - const projectRepository = this.dataSource.getRepository(Project) - const updateResult = await projectRepository.update({ id: projectId }, data) - - return Boolean(updateResult.affected) - } - - async deleteProjectById(projectId: string): Promise { - const projectRepository = this.dataSource.getRepository(Project) - const project = await projectRepository.findOneOrFail({ - where: { - id: projectId - }, - relations: { - projectMembers: true - } - }) - - const deleteResult = await projectRepository.softRemove(project) - - return Boolean(deleteResult) - } - - async deleteDomainById(domainId: string): Promise { - const domainRepository = this.dataSource.getRepository(Domain) - - const deleteResult = await domainRepository.softDelete({ id: domainId }) - - if (deleteResult.affected) { - return deleteResult.affected > 0 - } - - return false - } - - async addDomain(data: DeepPartial): Promise { - const domainRepository = this.dataSource.getRepository(Domain) - const newDomain = await domainRepository.save(data) - - return newDomain - } - - async getDomain(options: FindOneOptions): Promise { - const domainRepository = this.dataSource.getRepository(Domain) - const domain = await domainRepository.findOne(options) - - return domain - } - - async updateDomainById( - domainId: string, - data: DeepPartial - ): Promise { - const domainRepository = this.dataSource.getRepository(Domain) - const updateResult = await domainRepository.update({ id: domainId }, data) - - return Boolean(updateResult.affected) - } - - async getDomainsByProjectId( - projectId: string, - filter?: FindOptionsWhere - ): Promise { - const domainRepository = this.dataSource.getRepository(Domain) - - const domains = await domainRepository.find({ - relations: { - redirectTo: true - }, - where: { - project: { - id: projectId - }, - ...filter - } - }) - - return domains - } - - async getOldestDomainByProjectId(projectId: string): Promise { - const domainRepository = this.dataSource.getRepository(Domain) - - const domain = await domainRepository.findOne({ - where: { - project: { - id: projectId - } - }, - order: { - createdAt: 'ASC' - } - }) - - return domain - } - - async getLatestDNSRecordByProjectId( - projectId: string - ): Promise { - const deploymentRepository = this.dataSource.getRepository(Deployment) - - const deployment = await deploymentRepository.findOne({ - where: { - project: { - id: projectId - }, - status: DeploymentStatus.Ready - }, - order: { - createdAt: 'DESC' - } - }) - - if (deployment === null) { - throw new Error(`No deployment found for project ${projectId}`) - } - - return deployment.dnsRecordData - } - - async addDeployer(data: DeepPartial): Promise { - const deployerRepository = this.dataSource.getRepository(Deployer) - const newDomain = await deployerRepository.save(data) - - return newDomain - } - - async getDeployers(): Promise { - const deployerRepository = this.dataSource.getRepository(Deployer) - const deployers = await deployerRepository.find() - return deployers - } - - async getDeployerByLRN(deployerLrn: string): Promise { - const deployerRepository = this.dataSource.getRepository(Deployer) - const deployer = await deployerRepository.findOne({ - where: { deployerLrn } - }) - - return deployer - } -} diff --git a/apps/backend/src/entity/Deployer.ts b/apps/backend/src/entity/Deployer.ts deleted file mode 100644 index 95feb76..0000000 --- a/apps/backend/src/entity/Deployer.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Column, Entity, ManyToMany, PrimaryColumn } from 'typeorm' -import { Project } from './Project' - -@Entity() -export class Deployer { - @PrimaryColumn('varchar') - deployerLrn!: string - - @Column('varchar') - deployerId!: string - - @Column('varchar') - deployerApiUrl!: string - - @Column('varchar') - baseDomain!: string - - @Column('varchar', { nullable: true }) - publicKey!: string | null - - @Column('varchar', { nullable: true }) - minimumPayment!: string | null - - @Column('varchar', { nullable: true }) - paymentAddress!: string | null - - @ManyToMany( - () => Project, - (project) => project.deployers - ) - projects!: Project[] -} diff --git a/apps/backend/src/entity/Deployment.ts b/apps/backend/src/entity/Deployment.ts deleted file mode 100644 index 5e02678..0000000 --- a/apps/backend/src/entity/Deployment.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - JoinColumn, - ManyToOne, - PrimaryColumn, - UpdateDateColumn -} from 'typeorm' - -import type { - AppDeploymentRecordAttributes, - AppDeploymentRemovalRecordAttributes, - DNSRecordAttributes -} from '../types' -import { Deployer } from './Deployer' -import { Project } from './Project' -import { User } from './User' - -export enum Environment { - Production = 'Production', - Preview = 'Preview', - Development = 'Development' -} - -export enum DeploymentStatus { - Building = 'Building', - Ready = 'Ready', - Error = 'Error', - Deleting = 'Deleting' -} - -export interface ApplicationDeploymentRequest { - type: string - version: string - name: string - application: string - lrn?: string - auction?: string - config: string - meta: string - payment?: string - dns?: string -} - -export interface ApplicationDeploymentRemovalRequest { - type: string - version: string - deployment: string - auction?: string - payment?: string -} - -export interface ApplicationRecord { - type: string - version: string - name: string - description?: string - homepage?: string - license?: string - author?: string - repository?: string[] - app_version?: string - repository_ref: string - app_type: string -} - -@Entity() -export class Deployment { - // TODO: set custom generated id - @PrimaryColumn('varchar') - id!: string - - @Column() - projectId!: string - - @ManyToOne(() => Project, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'projectId' }) - project!: Project - - @Column('varchar') - branch!: string - - @Column('varchar') - commitHash!: string - - @Column('varchar') - commitMessage!: string - - @Column('varchar', { nullable: true }) - url!: string | null - - @Column('varchar') - applicationRecordId!: string - - @Column('simple-json') - applicationRecordData!: ApplicationRecord - - @Column('varchar', { nullable: true }) - applicationDeploymentRequestId!: string | null - - @Column('simple-json', { nullable: true }) - applicationDeploymentRequestData!: ApplicationDeploymentRequest | null - - @Column('varchar', { nullable: true }) - applicationDeploymentRecordId!: string | null - - @Column('simple-json', { nullable: true }) - applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null - - @Column('varchar', { nullable: true }) - applicationDeploymentRemovalRequestId!: string | null - - @Column('simple-json', { nullable: true }) - applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null - - @Column('varchar', { nullable: true }) - applicationDeploymentRemovalRecordId!: string | null - - @Column('simple-json', { nullable: true }) - applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null - - @Column('simple-json', { nullable: true }) - dnsRecordData!: DNSRecordAttributes | null - - @ManyToOne(() => Deployer) - @JoinColumn({ name: 'deployerLrn' }) - deployer!: Deployer - - @Column({ - enum: Environment - }) - environment!: Environment - - @Column('boolean', { default: false }) - isCurrent!: boolean - - @Column('boolean', { default: false }) - isCanonical!: boolean - - @Column({ - enum: DeploymentStatus - }) - status!: DeploymentStatus - - @ManyToOne(() => User) - @JoinColumn({ name: 'createdBy' }) - createdBy!: User - - @CreateDateColumn() - createdAt!: Date - - @UpdateDateColumn() - updatedAt!: Date - - @DeleteDateColumn() - deletedAt!: Date | null -} diff --git a/apps/backend/src/entity/Domain.ts b/apps/backend/src/entity/Domain.ts deleted file mode 100644 index 93a263f..0000000 --- a/apps/backend/src/entity/Domain.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn -} from 'typeorm' - -import { Project } from './Project' - -export enum Status { - Live = 'Live', - Pending = 'Pending' -} - -@Entity() -export class Domain { - @PrimaryGeneratedColumn('uuid') - id!: string - - @Column('varchar') - projectId!: string - - @ManyToOne(() => Project, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'projectId' }) - project!: Project - - @Column('varchar', { length: 255, default: 'main' }) - branch!: string - - @Column('varchar', { length: 255 }) - name!: string - - @Column('string', { nullable: true }) - redirectToId!: string | null - - @ManyToOne(() => Domain) - @JoinColumn({ name: 'redirectToId' }) - // eslint-disable-next-line no-use-before-define - redirectTo!: Domain | null - - @Column({ - enum: Status, - default: Status.Pending - }) - status!: Status - - @CreateDateColumn() - createdAt!: Date - - @UpdateDateColumn() - updatedAt!: Date - - @DeleteDateColumn() - deletedAt!: Date | null -} diff --git a/apps/backend/src/entity/EnvironmentVariable.ts b/apps/backend/src/entity/EnvironmentVariable.ts deleted file mode 100644 index 45ef5eb..0000000 --- a/apps/backend/src/entity/EnvironmentVariable.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn -} from 'typeorm' - -import { Project } from './Project' - -enum Environment { - Production = 'Production', - Preview = 'Preview', - Development = 'Development' -} - -@Entity() -export class EnvironmentVariable { - @PrimaryGeneratedColumn('uuid') - id!: string - - @ManyToOne(() => Project, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'projectId' }) - project!: Project - - @Column({ - enum: Environment - }) - environment!: Environment - - @Column('varchar') - key!: string - - @Column('varchar') - value!: string - - @CreateDateColumn() - createdAt!: Date - - @UpdateDateColumn() - updatedAt!: Date -} diff --git a/apps/backend/src/entity/Organization.ts b/apps/backend/src/entity/Organization.ts deleted file mode 100644 index b2fe727..0000000 --- a/apps/backend/src/entity/Organization.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - OneToMany, - PrimaryGeneratedColumn, - Unique, - UpdateDateColumn -} from 'typeorm' -import { UserOrganization } from './UserOrganization' - -@Entity() -@Unique(['slug']) -export class Organization { - @PrimaryGeneratedColumn('uuid') - id!: string - - @Column('varchar', { length: 255 }) - name!: string - - @Column('varchar') - slug!: string - - @CreateDateColumn() - createdAt!: Date - - @UpdateDateColumn() - updatedAt!: Date - - @OneToMany( - () => UserOrganization, - (userOrganization) => userOrganization.organization, - { - cascade: ['soft-remove'] - } - ) - userOrganizations!: UserOrganization[] -} diff --git a/apps/backend/src/entity/Project.ts b/apps/backend/src/entity/Project.ts deleted file mode 100644 index 7de2418..0000000 --- a/apps/backend/src/entity/Project.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - JoinColumn, - JoinTable, - ManyToMany, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - UpdateDateColumn -} from 'typeorm' - -import { Deployer } from './Deployer' -import { Deployment } from './Deployment' -import { Organization } from './Organization' -import { ProjectMember } from './ProjectMember' -import { User } from './User' - -@Entity() -export class Project { - @PrimaryGeneratedColumn('uuid') - id!: string - - @ManyToOne(() => User) - @JoinColumn({ name: 'ownerId' }) - owner!: User - - @Column({ nullable: false }) - ownerId!: string - - @ManyToOne(() => Organization, { nullable: true }) - @JoinColumn({ name: 'organizationId' }) - organization!: Organization | null - - @Column('varchar') - organizationId!: string - - @Column('varchar') - name!: string - - @Column('varchar') - repository!: string - - @Column('varchar', { length: 255, default: 'main' }) - prodBranch!: string - - @Column('text', { default: '' }) - description!: string - - @Column('varchar', { nullable: true }) - auctionId!: string | null - - // Tx hash for sending coins from snowball to deployer - @Column('varchar', { nullable: true }) - txHash!: string | null - - @ManyToMany( - () => Deployer, - (deployer) => deployer.projects - ) - @JoinTable() - deployers!: Deployer[] - - @Column('boolean', { default: false, nullable: true }) - fundsReleased!: boolean - - // TODO: Compute template & framework in import repository - @Column('varchar', { nullable: true }) - template!: string | null - - @Column('varchar', { nullable: true }) - framework!: string | null - - // Address of the user who created the project i.e. requested deployments - @Column('varchar') - paymentAddress!: string - - @Column({ - type: 'simple-array' - }) - webhooks!: string[] - - @Column('varchar') - icon!: string - - @CreateDateColumn() - createdAt!: Date - - @UpdateDateColumn() - updatedAt!: Date - - @DeleteDateColumn() - deletedAt!: Date | null - - @OneToMany( - () => Deployment, - (deployment) => deployment.project - ) - deployments!: Deployment[] - - @OneToMany( - () => ProjectMember, - (projectMember) => projectMember.project, - { - cascade: ['soft-remove'] - } - ) - projectMembers!: ProjectMember[] -} diff --git a/apps/backend/src/entity/ProjectMember.ts b/apps/backend/src/entity/ProjectMember.ts deleted file mode 100644 index 0e75ee0..0000000 --- a/apps/backend/src/entity/ProjectMember.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - Unique, - UpdateDateColumn -} from 'typeorm' - -import { Project } from './Project' -import { User } from './User' - -export enum Permission { - View = 'View', - Edit = 'Edit' -} - -@Entity() -@Unique(['project', 'member']) -export class ProjectMember { - @PrimaryGeneratedColumn('uuid') - id!: string - - @ManyToOne( - () => User, - (user) => user.projectMembers - ) - @JoinColumn({ name: 'userId' }) - member!: User - - @ManyToOne( - () => Project, - (project) => project.projectMembers - ) - @JoinColumn({ name: 'projectId' }) - project!: Project - - @Column({ - type: 'simple-array' - }) - permissions!: Permission[] - - @Column('boolean', { default: false }) - isPending!: boolean - - @CreateDateColumn() - createdAt!: Date - - @UpdateDateColumn() - updatedAt!: Date - - @DeleteDateColumn() - deletedAt!: Date | null -} diff --git a/apps/backend/src/entity/User.ts b/apps/backend/src/entity/User.ts deleted file mode 100644 index 86cdc76..0000000 --- a/apps/backend/src/entity/User.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - OneToMany, - PrimaryGeneratedColumn, - Unique -} from 'typeorm' - -import { ProjectMember } from './ProjectMember' -import { UserOrganization } from './UserOrganization' - -@Entity() -@Unique(['email']) -@Unique(['ethAddress']) -export class User { - @PrimaryGeneratedColumn('uuid') - id!: string - - // TODO: Set ethAddress as ID - @Column() - ethAddress!: string - - @Column('varchar', { length: 255, nullable: true }) - name!: string | null - - @Column() - email!: string - - @Column('varchar', { nullable: true }) - gitHubToken!: string | null - - @Column('boolean', { default: false }) - isVerified!: boolean - - @CreateDateColumn() - createdAt!: Date - - @CreateDateColumn() - updatedAt!: Date - - @Column() - subOrgId!: string - - @Column() - turnkeyWalletId!: string - - @OneToMany( - () => ProjectMember, - (projectMember) => projectMember.project, - { - cascade: ['soft-remove'] - } - ) - projectMembers!: ProjectMember[] - - @OneToMany( - () => UserOrganization, - (UserOrganization) => UserOrganization.member, - { - cascade: ['soft-remove'] - } - ) - userOrganizations!: UserOrganization[] -} diff --git a/apps/backend/src/entity/UserOrganization.ts b/apps/backend/src/entity/UserOrganization.ts deleted file mode 100644 index 2f34491..0000000 --- a/apps/backend/src/entity/UserOrganization.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn -} from 'typeorm' - -import { Organization } from './Organization' -import { User } from './User' - -export enum Role { - Owner = 'Owner', - Maintainer = 'Maintainer', - Reader = 'Reader' -} - -@Entity() -export class UserOrganization { - @PrimaryGeneratedColumn('uuid') - id!: string - - @ManyToOne(() => User) - @JoinColumn({ name: 'userId' }) - member!: User - - @ManyToOne(() => Organization) - @JoinColumn({ name: 'organizationId' }) - organization!: Organization - - @Column({ - enum: Role - }) - role!: Role - - @CreateDateColumn() - createdAt!: Date - - @UpdateDateColumn() - updatedAt!: Date - - @DeleteDateColumn() - deletedAt!: Date | null -} diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts deleted file mode 100644 index 5195e9b..0000000 --- a/apps/backend/src/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import 'express-async-errors' -import 'reflect-metadata' -import fs from 'node:fs' -import path from 'node:path' -import debug from 'debug' - -import { OAuthApp } from '@octokit/oauth-app' - -import { Database } from './database' -import { Registry } from './registry' -import { createResolvers } from './resolvers' -import { createAndStartServer } from './server' -import { Service } from './service' -import { getConfig } from './utils' - -const log = debug('snowball:server') -const OAUTH_CLIENT_TYPE = 'oauth-app' - -export const main = async (): Promise => { - const { server, database, gitHub, registryConfig } = await getConfig() - - const app = new OAuthApp({ - clientType: OAUTH_CLIENT_TYPE, - clientId: gitHub.oAuth.clientId, - clientSecret: gitHub.oAuth.clientSecret - }) - - const db = new Database(database) - await db.init() - - const registry = new Registry(registryConfig) - const service = new Service( - { gitHubConfig: gitHub, registryConfig }, - db, - app, - registry - ) - - const typeDefs = fs - .readFileSync(path.join(__dirname, 'schema.gql')) - .toString() - const resolvers = await createResolvers(service) - - await createAndStartServer(server, typeDefs, resolvers, service) -} - -main() - .then(() => { - log('Starting server...') - }) - .catch((err) => { - log(err) - }) diff --git a/apps/backend/src/registry.ts b/apps/backend/src/registry.ts deleted file mode 100644 index b74ce8b..0000000 --- a/apps/backend/src/registry.ts +++ /dev/null @@ -1,624 +0,0 @@ -import assert from 'node:assert' -import debug from 'debug' -import { DateTime } from 'luxon' -import type { Octokit } from 'octokit' -import * as openpgp from 'openpgp' -import { inc as semverInc } from 'semver' -import type { DeepPartial } from 'typeorm' - -import { - Account, - DEFAULT_GAS_ESTIMATION_MULTIPLIER, - Registry as LaconicRegistry, - getGasPrice, - parseGasAndFees -} from '@cerc-io/registry-sdk' -import type { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate' - -import type { RegistryConfig } from './config' -import type { - ApplicationDeploymentRemovalRequest, - ApplicationDeploymentRequest, - ApplicationRecord, - Deployment -} from './entity/Deployment' -import type { - AppDeploymentRecord, - AppDeploymentRemovalRecord, - AuctionParams, - DeployerRecord, - RegistryRecord -} from './types' -import { - getConfig, - getRepoDetails, - registryTransactionWithRetry, - sleep -} from './utils' - -const log = debug('snowball:registry') - -const APP_RECORD_TYPE = 'ApplicationRecord' -const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction' -const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest' -const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = - 'ApplicationDeploymentRemovalRequest' -const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord' -const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord' -const WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer' -const SLEEP_DURATION = 1000 - -// TODO: Move registry code to registry-sdk/watcher-ts -export class Registry { - private registry: LaconicRegistry - private registryConfig: RegistryConfig - - constructor(registryConfig: RegistryConfig) { - this.registryConfig = registryConfig - - const gasPrice = getGasPrice(registryConfig.fee.gasPrice) - - this.registry = new LaconicRegistry( - registryConfig.gqlEndpoint, - registryConfig.restEndpoint, - { chainId: registryConfig.chainId, gasPrice } - ) - } - - async createApplicationRecord({ - octokit, - repository, - commitHash, - appType - }: { - octokit: Octokit - repository: string - commitHash: string - appType: string - }): Promise<{ - applicationRecordId: string - applicationRecordData: ApplicationRecord - }> { - const { repo, repoUrl, packageJSON } = await getRepoDetails( - octokit, - repository, - commitHash - ) - // Use registry-sdk to publish record - // Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh - // Fetch previous records - const records = await this.registry.queryRecords( - { - type: APP_RECORD_TYPE, - name: packageJSON.name - }, - true - ) - - // Get next version of record - const bondRecords = records.filter( - (record: any) => record.bondId === this.registryConfig.bondId - ) - const [latestBondRecord] = bondRecords.sort( - (a: any, b: any) => - new Date(b.createTime).getTime() - new Date(a.createTime).getTime() - ) - const nextVersion = semverInc( - latestBondRecord?.attributes.version ?? '0.0.0', - 'patch' - ) - - assert(nextVersion, 'Application record version not valid') - - // Create record of type ApplicationRecord and publish - const applicationRecord = { - type: APP_RECORD_TYPE, - version: nextVersion, - repository_ref: commitHash, - repository: [repoUrl], - app_type: appType, - name: repo, - ...(packageJSON.description && { description: packageJSON.description }), - ...(packageJSON.homepage && { homepage: packageJSON.homepage }), - ...(packageJSON.license && { license: packageJSON.license }), - ...(packageJSON.author && { - author: - typeof packageJSON.author === 'object' - ? JSON.stringify(packageJSON.author) - : packageJSON.author - }), - ...(packageJSON.version && { app_version: packageJSON.version }) - } - - const result = await this.publishRecord(applicationRecord) - - log(`Published application record ${result.id}`) - log('Application record data:', applicationRecord) - - // TODO: Discuss computation of LRN - const lrn = this.getLrn(repo) - log(`Setting name: ${lrn} for record ID: ${result.id}`) - - const fee = parseGasAndFees( - this.registryConfig.fee.gas, - this.registryConfig.fee.fees - ) - - await sleep(SLEEP_DURATION) - await registryTransactionWithRetry(() => - this.registry.setName( - { - cid: result.id, - lrn - }, - this.registryConfig.privateKey, - fee - ) - ) - - await sleep(SLEEP_DURATION) - await registryTransactionWithRetry(() => - this.registry.setName( - { - cid: result.id, - lrn: `${lrn}@${applicationRecord.app_version}` - }, - this.registryConfig.privateKey, - fee - ) - ) - - await sleep(SLEEP_DURATION) - await registryTransactionWithRetry(() => - this.registry.setName( - { - cid: result.id, - lrn: `${lrn}@${applicationRecord.repository_ref}` - }, - this.registryConfig.privateKey, - fee - ) - ) - - return { - applicationRecordId: result.id, - applicationRecordData: applicationRecord - } - } - - async createApplicationDeploymentAuction( - appName: string, - octokit: Octokit, - auctionParams: AuctionParams, - data: DeepPartial - ): Promise<{ - applicationDeploymentAuctionId: string - }> { - assert(data.project?.repository, 'Project repository not found') - - await this.createApplicationRecord({ - octokit, - repository: data.project.repository, - appType: data.project!.template!, - commitHash: data.commitHash! - }) - - const lrn = this.getLrn(appName) - const config = await getConfig() - const auctionConfig = config.auction - - const fee = parseGasAndFees( - this.registryConfig.fee.gas, - this.registryConfig.fee.fees - ) - const auctionResult = await registryTransactionWithRetry(() => - this.registry.createProviderAuction( - { - commitFee: auctionConfig.commitFee, - commitsDuration: auctionConfig.commitsDuration, - revealFee: auctionConfig.revealFee, - revealsDuration: auctionConfig.revealsDuration, - denom: auctionConfig.denom, - maxPrice: auctionParams.maxPrice, - numProviders: auctionParams.numProviders - }, - this.registryConfig.privateKey, - fee - ) - ) - - if (!auctionResult.auction) { - throw new Error('Error creating auction') - } - - // Create record of type applicationDeploymentAuction and publish - const applicationDeploymentAuction = { - application: lrn, - auction: auctionResult.auction.id, - type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE - } - - const result = await this.publishRecord(applicationDeploymentAuction) - - log(`Application deployment auction created: ${auctionResult.auction.id}`) - log(`Application deployment auction record published: ${result.id}`) - log('Application deployment auction data:', applicationDeploymentAuction) - - return { - applicationDeploymentAuctionId: auctionResult.auction.id - } - } - - async createApplicationDeploymentRequest(data: { - deployment: Deployment - appName: string - repository: string - auctionId?: string | null - lrn: string - apiUrl: string - environmentVariables: { [key: string]: string } - dns: string - requesterAddress: string - publicKey: string - payment?: string | null - }): Promise<{ - applicationDeploymentRequestId: string - applicationDeploymentRequestData: ApplicationDeploymentRequest - }> { - const lrn = this.getLrn(data.appName) - const records = await this.registry.resolveNames([lrn]) - const applicationRecord = records[0] - - if (!applicationRecord) { - throw new Error(`No record found for ${lrn}`) - } - - let hash: string | undefined - if (Object.keys(data.environmentVariables).length !== 0) { - hash = await this.generateConfigHash( - data.environmentVariables, - data.requesterAddress, - data.publicKey, - data.apiUrl - ) - } - - // Create record of type ApplicationDeploymentRequest and publish - const applicationDeploymentRequest = { - type: APP_DEPLOYMENT_REQUEST_TYPE, - version: '1.0.0', - name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`, - application: `${lrn}@${applicationRecord.attributes.app_version}`, - dns: data.dns, - - // https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b - config: JSON.stringify(hash ? { ref: hash } : {}), - meta: JSON.stringify({ - note: `Added by Snowball @ ${DateTime.utc().toFormat( - "EEE LLL dd HH:mm:ss 'UTC' yyyy" - )}`, - repository: data.repository, - repository_ref: data.deployment.commitHash - }), - deployer: data.lrn, - ...(data.auctionId && { auction: data.auctionId }), - ...(data.payment && { payment: data.payment }) - } - - await sleep(SLEEP_DURATION) - - const result = await this.publishRecord(applicationDeploymentRequest) - - log(`Application deployment request record published: ${result.id}`) - log('Application deployment request data:', applicationDeploymentRequest) - - return { - applicationDeploymentRequestId: result.id, - applicationDeploymentRequestData: applicationDeploymentRequest - } - } - - async getAuctionWinningDeployerRecords( - auctionId: string - ): Promise { - const records = await this.registry.getAuctionsByIds([auctionId]) - const auctionResult = records[0] - - const deployerRecords = [] - const { winnerAddresses } = auctionResult - - for (const auctionWinner of winnerAddresses) { - const records = await this.getDeployerRecordsByFilter({ - paymentAddress: auctionWinner - }) - - const newRecords = records.filter((record) => { - return record.names !== null && record.names.length > 0 - }) - - for (const record of newRecords) { - if (record.id) { - deployerRecords.push(record) - break - } - } - } - - return deployerRecords - } - - async releaseDeployerFunds(auctionId: string): Promise { - const fee = parseGasAndFees( - this.registryConfig.fee.gas, - this.registryConfig.fee.fees - ) - const auction = await registryTransactionWithRetry(() => - this.registry.releaseFunds( - { - auctionId - }, - this.registryConfig.privateKey, - fee - ) - ) - - return auction - } - - /** - * Fetch ApplicationDeploymentRecords for deployments - */ - async getDeploymentRecords( - deployments: Deployment[] - ): Promise { - // Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments - // TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute - const records = await this.registry.queryRecords( - { - type: APP_DEPLOYMENT_RECORD_TYPE - }, - true - ) - - // Filter records with ApplicationDeploymentRequestId ID - return records.filter((record: AppDeploymentRecord) => - deployments.some( - (deployment) => - deployment.applicationDeploymentRequestId === - record.attributes.request - ) - ) - } - - /** - * Fetch WebappDeployer Records by filter - */ - async getDeployerRecordsByFilter(filter: { [key: string]: any }): Promise< - DeployerRecord[] - > { - return this.registry.queryRecords( - { - type: WEBAPP_DEPLOYER_RECORD_TYPE, - ...filter - }, - true - ) - } - - /** - * Fetch ApplicationDeploymentRecords by filter - */ - async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise< - AppDeploymentRecord[] - > { - return this.registry.queryRecords( - { - type: APP_DEPLOYMENT_RECORD_TYPE, - ...filter - }, - true - ) - } - - /** - * Fetch ApplicationDeploymentRemovalRecords for deployments - */ - async getDeploymentRemovalRecords( - deployments: Deployment[] - ): Promise { - // Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments - const records = await this.registry.queryRecords( - { - type: APP_DEPLOYMENT_REMOVAL_RECORD_TYPE - }, - true - ) - - // Filter records with ApplicationDeploymentRecord and ApplicationDeploymentRemovalRequest IDs - return records.filter((record: AppDeploymentRemovalRecord) => - deployments.some( - (deployment) => - deployment.applicationDeploymentRemovalRequestId === - record.attributes.request && - deployment.applicationDeploymentRecordId === - record.attributes.deployment - ) - ) - } - - /** - * Fetch record by Id - */ - async getRecordById(id: string): Promise { - const [record] = await this.registry.getRecordsByIds([id]) - return record ?? null - } - - async createApplicationDeploymentRemovalRequest(data: { - deploymentId: string - deployerLrn: string - auctionId?: string | null - payment?: string | null - }): Promise<{ - applicationDeploymentRemovalRequestId: string - applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest - }> { - const applicationDeploymentRemovalRequest = { - type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE, - version: '1.0.0', - deployment: data.deploymentId, - deployer: data.deployerLrn, - ...(data.auctionId && { auction: data.auctionId }), - ...(data.payment && { payment: data.payment }) - } - - const result = await this.publishRecord(applicationDeploymentRemovalRequest) - - log(`Application deployment removal request record published: ${result.id}`) - log( - 'Application deployment removal request data:', - applicationDeploymentRemovalRequest - ) - - return { - applicationDeploymentRemovalRequestId: result.id, - applicationDeploymentRemovalRequestData: - applicationDeploymentRemovalRequest - } - } - - async getCompletedAuctionIds(auctionIds: string[]): Promise { - if (auctionIds.length === 0) { - return [] - } - - const auctions = await this.registry.getAuctionsByIds(auctionIds) - - const completedAuctions = auctions - .filter( - (auction: { id: string; status: string }) => - auction.status === 'completed' - ) - .map((auction: { id: string; status: string }) => auction.id) - - return completedAuctions - } - - async publishRecord(recordData: any): Promise { - const fee = parseGasAndFees( - this.registryConfig.fee.gas, - this.registryConfig.fee.fees - ) - - const result = await registryTransactionWithRetry(() => - this.registry.setRecord( - { - privateKey: this.registryConfig.privateKey, - record: recordData, - bondId: this.registryConfig.bondId - }, - this.registryConfig.privateKey, - fee - ) - ) - - return result - } - - async getRecordsByName(name: string): Promise { - return this.registry.resolveNames([name]) - } - - async getAuctionData(auctionId: string): Promise { - return this.registry.getAuctionsByIds([auctionId]) - } - - async sendTokensToAccount( - receiverAddress: string, - amount: string - ): Promise { - const fee = parseGasAndFees( - this.registryConfig.fee.gas, - this.registryConfig.fee.fees - ) - const account = await this.getAccount() - const laconicClient = await this.registry.getLaconicClient(account) - const txResponse: DeliverTxResponse = await registryTransactionWithRetry( - () => - laconicClient.sendTokens( - account.address, - receiverAddress, - [ - { - denom: 'alnt', - amount - } - ], - fee || DEFAULT_GAS_ESTIMATION_MULTIPLIER - ) - ) - - return txResponse - } - - async getAccount(): Promise { - const account = new Account( - Buffer.from(this.registryConfig.privateKey, 'hex') - ) - await account.init() - - return account - } - - async getTxResponse(txHash: string): Promise { - const account = await this.getAccount() - const laconicClient = await this.registry.getLaconicClient(account) - const txResponse: IndexedTx | null = await laconicClient.getTx(txHash) - - return txResponse - } - - getLrn(appName: string): string { - assert(this.registryConfig.authority, "Authority doesn't exist") - return `lrn://${this.registryConfig.authority}/applications/${appName}` - } - - async generateConfigHash( - environmentVariables: { [key: string]: string }, - requesterAddress: string, - pubKey: string, - url: string - ): Promise { - // Config to be encrypted - const config = { - authorized: [requesterAddress], - config: { env: environmentVariables } - } - - // Serialize the config - const serialized = JSON.stringify(config, null, 2) - - const armoredKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n${pubKey}\n\n-----END PGP PUBLIC KEY BLOCK-----` - const publicKey = await openpgp.readKey({ armoredKey }) - - // Encrypt the config - const encrypted = await openpgp.encrypt({ - message: await openpgp.createMessage({ text: serialized }), - encryptionKeys: publicKey, - format: 'binary' - }) - - // Get the hash after uploading encrypted config - const response = await fetch(`${url}/upload/config`, { - method: 'POST', - headers: { - 'Content-Type': 'application/octet-stream' - }, - body: encrypted - }) - - const configHash = await response.json() - - return configHash.id - } -} diff --git a/apps/backend/src/resolvers.ts b/apps/backend/src/resolvers.ts deleted file mode 100644 index 38a4a31..0000000 --- a/apps/backend/src/resolvers.ts +++ /dev/null @@ -1,413 +0,0 @@ -import debug from 'debug' -import type { DeepPartial, FindOptionsWhere } from 'typeorm' - -import type { Domain } from './entity/Domain' -import type { EnvironmentVariable } from './entity/EnvironmentVariable' -import type { Project } from './entity/Project' -import type { Permission } from './entity/ProjectMember' -import type { Service } from './service' -import type { - AddProjectFromTemplateInput, - AuctionParams, - EnvironmentVariables -} from './types' - -const log = debug('snowball:resolver') - -export const createResolvers = async (service: Service): Promise => { - return { - Query: { - // TODO: add custom type for context - user: (_: any, __: any, context: any) => { - return context.user - }, - - organizations: async (_: any, __: any, context: any) => { - return service.getOrganizationsByUserId(context.user) - }, - - project: async ( - _: any, - { projectId }: { projectId: string }, - context: any - ) => { - return service.getProjectById(context.user, projectId) - }, - - projectsInOrganization: async ( - _: any, - { organizationSlug }: { organizationSlug: string }, - context: any - ) => { - return service.getProjectsInOrganization(context.user, organizationSlug) - }, - - deployments: async (_: any, { projectId }: { projectId: string }) => { - return service.getNonCanonicalDeploymentsByProjectId(projectId) - }, - - environmentVariables: async ( - _: any, - { projectId }: { projectId: string } - ) => { - return service.getEnvironmentVariablesByProjectId(projectId) - }, - - projectMembers: async (_: any, { projectId }: { projectId: string }) => { - return service.getProjectMembersByProjectId(projectId) - }, - - searchProjects: async ( - _: any, - { searchText }: { searchText: string }, - context: any - ) => { - return service.searchProjects(context.user, searchText) - }, - - domains: async ( - _: any, - { - projectId, - filter - }: { projectId: string; filter?: FindOptionsWhere } - ) => { - return service.getDomainsByProjectId(projectId, filter) - }, - - getAuctionData: async (_: any, { auctionId }: { auctionId: string }) => { - return service.getAuctionData(auctionId) - }, - - deployers: async (_: any, __: any) => { - return service.getDeployers() - }, - - address: async (_: any, __: any) => { - return service.getAddress() - }, - - verifyTx: async ( - _: any, - { - txHash, - amount, - senderAddress - }: { txHash: string; amount: string; senderAddress: string } - ) => { - return service.verifyTx(txHash, amount, senderAddress) - }, - - latestDNSRecord: async (_: any, { projectId }: { projectId: string }) => { - return service.getLatestDNSRecordByProjectId(projectId) - } - }, - - // TODO: Return error in GQL response - Mutation: { - removeProjectMember: async ( - _: any, - { projectMemberId }: { projectMemberId: string }, - context: any - ) => { - try { - return await service.removeProjectMember( - context.user, - projectMemberId - ) - } catch (err) { - log(err) - return false - } - }, - - updateProjectMember: async ( - _: any, - { - projectMemberId, - data - }: { - projectMemberId: string - data: { - permissions: Permission[] - } - } - ) => { - try { - return await service.updateProjectMember(projectMemberId, data) - } catch (err) { - log(err) - return false - } - }, - - addProjectMember: async ( - _: any, - { - projectId, - data - }: { - projectId: string - data: { - email: string - permissions: Permission[] - } - } - ) => { - try { - return Boolean(await service.addProjectMember(projectId, data)) - } catch (err) { - log(err) - return false - } - }, - - addEnvironmentVariables: async ( - _: any, - { - projectId, - data - }: { - projectId: string - data: { environments: string[]; key: string; value: string }[] - } - ) => { - try { - return Boolean(await service.addEnvironmentVariables(projectId, data)) - } catch (err) { - log(err) - return false - } - }, - - updateEnvironmentVariable: async ( - _: any, - { - environmentVariableId, - data - }: { - environmentVariableId: string - data: DeepPartial - } - ) => { - try { - return await service.updateEnvironmentVariable( - environmentVariableId, - data - ) - } catch (err) { - log(err) - return false - } - }, - - removeEnvironmentVariable: async ( - _: any, - { environmentVariableId }: { environmentVariableId: string } - ) => { - try { - return await service.removeEnvironmentVariable(environmentVariableId) - } catch (err) { - log(err) - return false - } - }, - - updateDeploymentToProd: async ( - _: any, - { deploymentId }: { deploymentId: string }, - context: any - ) => { - try { - return Boolean( - await service.updateDeploymentToProd(context.user, deploymentId) - ) - } catch (err) { - log(err) - return false - } - }, - - addProjectFromTemplate: async ( - _: any, - { - organizationSlug, - data, - lrn, - auctionParams, - environmentVariables - }: { - organizationSlug: string - data: AddProjectFromTemplateInput - lrn: string - auctionParams: AuctionParams - environmentVariables: EnvironmentVariables[] - }, - context: any - ) => { - try { - return await service.addProjectFromTemplate( - context.user, - organizationSlug, - data, - lrn, - auctionParams, - environmentVariables - ) - } catch (err) { - log(err) - throw err - } - }, - - addProject: async ( - _: any, - { - organizationSlug, - data, - lrn, - auctionParams, - environmentVariables - }: { - organizationSlug: string - data: DeepPartial - lrn: string - auctionParams: AuctionParams - environmentVariables: EnvironmentVariables[] - }, - context: any - ) => { - try { - return await service.addProject( - context.user, - organizationSlug, - data, - lrn, - auctionParams, - environmentVariables - ) - } catch (err) { - log(err) - throw err - } - }, - - updateProject: async ( - _: any, - { projectId, data }: { projectId: string; data: DeepPartial } - ) => { - try { - return await service.updateProject(projectId, data) - } catch (err) { - log(err) - return false - } - }, - - redeployToProd: async ( - _: any, - { deploymentId }: { deploymentId: string }, - context: any - ) => { - try { - return Boolean( - await service.redeployToProd(context.user, deploymentId) - ) - } catch (err) { - log(err) - return false - } - }, - - deleteProject: async (_: any, { projectId }: { projectId: string }) => { - try { - return await service.deleteProject(projectId) - } catch (err) { - log(err) - return false - } - }, - - deleteDomain: async (_: any, { domainId }: { domainId: string }) => { - try { - return await service.deleteDomain(domainId) - } catch (err) { - log(err) - return false - } - }, - - rollbackDeployment: async ( - _: any, - { projectId, deploymentId }: { deploymentId: string; projectId: string } - ) => { - try { - return await service.rollbackDeployment(projectId, deploymentId) - } catch (err) { - log(err) - return false - } - }, - - deleteDeployment: async ( - _: any, - { deploymentId }: { deploymentId: string } - ) => { - try { - return await service.deleteDeployment(deploymentId) - } catch (err) { - log(err) - return false - } - }, - - addDomain: async ( - _: any, - { projectId, data }: { projectId: string; data: { name: string } } - ) => { - try { - return Boolean(await service.addDomain(projectId, data)) - } catch (err) { - log(err) - return false - } - }, - - updateDomain: async ( - _: any, - { domainId, data }: { domainId: string; data: DeepPartial } - ) => { - try { - return await service.updateDomain(domainId, data) - } catch (err) { - log(err) - return false - } - }, - - authenticateGitHub: async ( - _: any, - { code }: { code: string }, - context: any - ) => { - try { - return await service.authenticateGitHub(code, context.user) - } catch (err) { - log(err) - return false - } - }, - - unauthenticateGitHub: async (_: any, __: object, context: any) => { - try { - return service.unauthenticateGitHub(context.user, { - gitHubToken: null - }) - } catch (err) { - log(err) - return false - } - } - } - } -} diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts deleted file mode 100644 index 3d2ff5d..0000000 --- a/apps/backend/src/routes/auth.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Router } from 'express' -import { SiweMessage } from 'siwe' -import type { Service } from '../service' -import { authenticateUser, createUser } from '../turnkey-backend' - -const router: Router = Router() - -// -// Turnkey -// -router.get('/registration/:email', async (req, res) => { - const service: Service = req.app.get('service') - const user = await service.getUserByEmail(req.params.email) - if (user) { - return res.send({ subOrganizationId: user?.subOrgId }) - } - - return res.sendStatus(204) -}) - -router.post('/register', async (req, res) => { - console.log('Register', req.body) - const { email, challenge, attestation } = req.body - const user = await createUser(req.app.get('service'), { - challenge, - attestation, - userEmail: email, - userName: email.split('@')[0] - }) - req.session.address = user.id - res.sendStatus(200) -}) - -router.post('/authenticate', async (req, res) => { - console.log('Authenticate', req.body) - const { signedWhoamiRequest } = req.body - const user = await authenticateUser( - req.app.get('service'), - signedWhoamiRequest - ) - if (user) { - req.session.address = user.id - res.sendStatus(200) - } else { - res.sendStatus(401) - } -}) - -// -// SIWE Auth -// -router.post('/validate', async (req, res) => { - const { message, signature } = req.body - const { success, data } = await new SiweMessage(message).verify({ - signature - }) - - if (!success) { - return res.send({ success }) - } - const service: Service = req.app.get('service') - const user = await service.getUserByEthAddress(data.address) - - if (!user) { - const newUser = await service.createUser({ - ethAddress: data.address, - email: `${data.address}@example.com`, - subOrgId: '', - turnkeyWalletId: '' - }) - - // SIWESession from the web3modal library requires both address and chain ID - req.session.address = newUser.id - req.session.chainId = data.chainId - } else { - req.session.address = user.id - req.session.chainId = data.chainId - } - - res.send({ success }) -}) - -// -// General -// -router.get('/session', (req, res) => { - if (req.session.address && req.session.chainId) { - res.send({ - address: req.session.address, - chainId: req.session.chainId - }) - } else { - res.status(401).send({ error: 'Unauthorized: No active session' }) - } -}) - -export default router diff --git a/apps/backend/src/routes/github.ts b/apps/backend/src/routes/github.ts deleted file mode 100644 index a64f023..0000000 --- a/apps/backend/src/routes/github.ts +++ /dev/null @@ -1,26 +0,0 @@ -import debug from 'debug' -import { Router } from 'express' - -import type { Service } from '../service' - -const log = debug('snowball:routes-github') -const router: Router = Router() - -/* POST GitHub webhook handler */ -// https://docs.github.com/en/webhooks/using-webhooks/handling-webhook-deliveries#javascript-example -router.post('/webhook', async (req, res) => { - // Server should respond with a 2XX response within 10 seconds of receiving a webhook delivery - // If server takes longer than that to respond, then GitHub terminates the connection and considers the delivery a failure - res.status(202).send('Accepted') - - const service = req.app.get('service') as Service - const githubEvent = req.headers['x-github-event'] - log(`Received GitHub webhook for event ${githubEvent}`) - - if (githubEvent === 'push') { - // Create deployments using push event data - await service.handleGitHubPush(req.body) - } -}) - -export default router diff --git a/apps/backend/src/routes/staging.ts b/apps/backend/src/routes/staging.ts deleted file mode 100644 index e6cbd0b..0000000 --- a/apps/backend/src/routes/staging.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Router } from 'express' - -const router: Router = Router() - -router.get('/version', async (_req, res) => { - return res.send({ version: '0.0.9' }) -}) - -export default router diff --git a/apps/backend/src/schema.gql b/apps/backend/src/schema.gql deleted file mode 100644 index 4dbff1d..0000000 --- a/apps/backend/src/schema.gql +++ /dev/null @@ -1,337 +0,0 @@ -enum Role { - Owner - Maintainer - Reader -} - -enum Permission { - View - Edit -} - -enum Environment { - Production - Preview - Development -} - -enum DeploymentStatus { - Building - Ready - Error - Deleting -} - -enum AuctionStatus { - completed - reveal - commit - expired -} - -enum DomainStatus { - Live - Pending -} - -type User { - id: String! - name: String - email: String! - organizations: [Organization!] - projects: [Project!] - isVerified: Boolean! - createdAt: String! - updatedAt: String! - gitHubToken: String -} - -type Organization { - id: String! - name: String! - slug: String! - projects: [Project!] - createdAt: String! - updatedAt: String! - members: [OrganizationMember!] -} - -type OrganizationMember { - id: String! - member: User! - role: Role! - createdAt: String! - updatedAt: String! -} - -type Project { - id: String! - owner: User! - deployments: [Deployment!] - name: String! - repository: String! - prodBranch: String! - description: String - deployers: [Deployer!] - auctionId: String - fundsReleased: Boolean - template: String - framework: String - paymentAddress: String! - txHash: String! - webhooks: [String!] - members: [ProjectMember!] - environmentVariables: [EnvironmentVariable!] - createdAt: String! - updatedAt: String! - organization: Organization! - icon: String - baseDomains: [String!] -} - -type ProjectMember { - id: String! - member: User! - permissions: [Permission!]! - isPending: Boolean! - createdAt: String! - updatedAt: String! -} - -type Deployment { - id: String! - branch: String! - commitHash: String! - commitMessage: String! - url: String - environment: Environment! - deployer: Deployer - applicationDeploymentRequestId: String - applicationDeploymentRecordData: AppDeploymentRecordAttributes - isCurrent: Boolean! - baseDomain: String - status: DeploymentStatus! - createdAt: String! - updatedAt: String! - createdBy: User! -} - -type Domain { - id: String! - branch: String! - name: String! - redirectTo: Domain - status: DomainStatus! - createdAt: String! - updatedAt: String! -} - -type EnvironmentVariable { - id: String! - environment: Environment! - key: String! - value: String! - createdAt: String! - updatedAt: String! -} - -type Deployer { - deployerLrn: String! - deployerId: String! - deployerApiUrl: String! - minimumPayment: String - paymentAddress: String - createdAt: String! - updatedAt: String! - baseDomain: String -} - -type AuthResult { - token: String! -} - -input AddEnvironmentVariableInput { - environments: [Environment!]! - key: String! - value: String! -} - -input AddProjectFromTemplateInput { - templateOwner: String! - templateRepo: String! - owner: String! - name: String! - isPrivate: Boolean! - paymentAddress: String! - txHash: String! -} - -input AddProjectInput { - name: String! - repository: String! - prodBranch: String! - template: String - paymentAddress: String! - txHash: String! -} - -input UpdateProjectInput { - name: String - description: String - prodBranch: String - organizationId: String - webhooks: [String!] -} - -input AddDomainInput { - name: String! -} - -input UpdateDomainInput { - name: String - branch: String - redirectToId: String -} - -input UpdateEnvironmentVariableInput { - key: String - value: String -} - -input AddProjectMemberInput { - email: String! - permissions: [Permission!] -} - -input UpdateProjectMemberInput { - permissions: [Permission] -} - -input FilterDomainsInput { - branch: String - status: DomainStatus -} - -type Fee { - type: String! - quantity: String! -} - -type Bid { - auctionId: String! - bidderAddress: String! - status: String! - commitHash: String! - commitTime: String - commitFee: Fee - revealTime: String - revealFee: Fee - bidAmount: Fee -} - -type Auction { - id: String! - kind: String! - status: String! - ownerAddress: String! - createTime: String! - commitsEndTime: String! - revealsEndTime: String! - commitFee: Fee! - revealFee: Fee! - minimumBid: Fee - winnerAddresses: [String!]! - winnerBids: [Fee!] - winnerPrice: Fee - maxPrice: Fee - numProviders: Int! - fundsReleased: Boolean! - bids: [Bid!]! -} - -type DNSRecordAttributes { - name: String - value: String - request: String - resourceType: String - version: String -} - -type AppDeploymentRecordAttributes { - application: String - auction: String - deployer: String - dns: String - meta: String - name: String - request: String - type: String - url: String - version: String -} - -input AuctionParams { - maxPrice: String - numProviders: Int -} - -type Query { - user: User! - organizations: [Organization!] - projects: [Project!] - projectsInOrganization(organizationSlug: String!): [Project!] - project(projectId: String!): Project - deployments(projectId: String!): [Deployment!] - environmentVariables(projectId: String!): [EnvironmentVariable!] - projectMembers(projectId: String!): [ProjectMember!] - searchProjects(searchText: String!): [Project!] - getAuctionData(auctionId: String!): Auction! - latestDNSRecord(projectId: String!): DNSRecordAttributes - domains(projectId: String!, filter: FilterDomainsInput): [Domain] - deployers: [Deployer] - address: String! - verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean! -} - -type Mutation { - addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean! - updateProjectMember( - projectMemberId: String! - data: UpdateProjectMemberInput - ): Boolean! - removeProjectMember(projectMemberId: String!): Boolean! - addEnvironmentVariables( - projectId: String! - data: [AddEnvironmentVariableInput!] - ): Boolean! - updateEnvironmentVariable( - environmentVariableId: String! - data: UpdateEnvironmentVariableInput! - ): Boolean! - removeEnvironmentVariable(environmentVariableId: String!): Boolean! - updateDeploymentToProd(deploymentId: String!): Boolean! - addProjectFromTemplate( - organizationSlug: String! - data: AddProjectFromTemplateInput - lrn: String - auctionParams: AuctionParams - environmentVariables: [AddEnvironmentVariableInput!] - ): Project! - addProject( - organizationSlug: String! - data: AddProjectInput! - lrn: String - auctionParams: AuctionParams - environmentVariables: [AddEnvironmentVariableInput!] - ): Project! - updateProject(projectId: String!, data: UpdateProjectInput): Boolean! - redeployToProd(deploymentId: String!): Boolean! - deleteProject(projectId: String!): Boolean! - deleteDomain(domainId: String!): Boolean! - rollbackDeployment(projectId: String!, deploymentId: String!): Boolean! - deleteDeployment(deploymentId: String!): Boolean! - addDomain(projectId: String!, data: AddDomainInput!): Boolean! - updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean! - authenticateGitHub(code: String!): AuthResult! - unauthenticateGitHub: Boolean! -} diff --git a/apps/backend/src/server.ts b/apps/backend/src/server.ts deleted file mode 100644 index af3ff87..0000000 --- a/apps/backend/src/server.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - ApolloServerPluginDrainHttpServer, - ApolloServerPluginLandingPageLocalDefault, - AuthenticationError -} from 'apollo-server-core' -import { ApolloServer } from 'apollo-server-express' -import cors from 'cors' -import debug from 'debug' -import express from 'express' -import session from 'express-session' -import { createServer } from 'node:http' - -import { makeExecutableSchema } from '@graphql-tools/schema' -import type { TypeSource } from '@graphql-tools/utils' - -import type { ServerConfig } from './config' -import authRouter from './routes/auth' -import githubRouter from './routes/github' -import stagingRouter from './routes/staging' -import type { Service } from './service' - -const log = debug('snowball:server') - -// Set cookie expiration to 1 month in milliseconds -const COOKIE_MAX_AGE = 30 * 24 * 60 * 60 * 1000 - -declare module 'express-session' { - interface SessionData { - address: string - chainId: number - } -} - -export const createAndStartServer = async ( - serverConfig: ServerConfig, - typeDefs: TypeSource, - resolvers: any, - service: Service -): Promise => { - const { host, port, gqlPath = '/graphql' } = serverConfig - const { appOriginUrl, secret, domain, trustProxy } = serverConfig.session - - const app = express() - - // Create HTTP server - const httpServer = createServer(app) - - // Create the schema - const schema = makeExecutableSchema({ - typeDefs, - resolvers - }) - - const server = new ApolloServer({ - schema, - csrfPrevention: true, - context: async ({ req }) => { - // https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization - - const { address } = req.session - - if (!address) { - throw new AuthenticationError('Unauthorized: No active session') - } - - const user = await service.getUser(address) - return { user } - }, - plugins: [ - // Proper shutdown for the HTTP server - ApolloServerPluginDrainHttpServer({ httpServer }), - ApolloServerPluginLandingPageLocalDefault({ embed: true }) - ] - }) - - await server.start() - - app.use( - cors({ - origin: appOriginUrl, - credentials: true - }) - ) - - const sessionOptions: session.SessionOptions = { - secret: secret, - resave: false, - saveUninitialized: true, - cookie: { - secure: new URL(appOriginUrl).protocol === 'https:', - maxAge: COOKIE_MAX_AGE, - domain: domain || undefined, - sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax' - } - } - - if (trustProxy) { - // trust first proxy - app.set('trust proxy', 1) - } - - app.use(session(sessionOptions)) - - server.applyMiddleware({ - app: app as any, - path: gqlPath, - cors: { - origin: [appOriginUrl], - credentials: true - } - }) - - app.use(express.json()) - - app.set('service', service) - app.use('/auth', authRouter) - app.use('/api/github', githubRouter) - app.use('/staging', stagingRouter) - - app.use((err: any, _req: any, res: any, _next: any) => { - console.error(err) - res.status(500).json({ error: err.message }) - }) - - httpServer.listen(port, host, () => { - log(`Server is listening on ${host}:${port}${server.graphqlPath}`) - }) - - return server -} diff --git a/apps/backend/src/service.ts b/apps/backend/src/service.ts deleted file mode 100644 index d7c1f26..0000000 --- a/apps/backend/src/service.ts +++ /dev/null @@ -1,1783 +0,0 @@ -import assert from 'node:assert' -import debug from 'debug' -import { DateTime } from 'luxon' - -import { Octokit, RequestError } from 'octokit' -import type { DeepPartial, FindOptionsWhere } from 'typeorm' - -import type { OAuthApp } from '@octokit/oauth-app' - -import type { GitHubConfig, RegistryConfig } from './config' -import type { Database } from './database' -import { Deployer } from './entity/Deployer' -import { - type ApplicationRecord, - type Deployment, - DeploymentStatus, - Environment -} from './entity/Deployment' -import type { Domain } from './entity/Domain' -import type { EnvironmentVariable } from './entity/EnvironmentVariable' -import type { Organization } from './entity/Organization' -import { Project } from './entity/Project' -import type { Permission, ProjectMember } from './entity/ProjectMember' -import { User } from './entity/User' -import { Role } from './entity/UserOrganization' -import type { Registry } from './registry' -import type { - AddProjectFromTemplateInput, - AppDeploymentRecord, - AppDeploymentRemovalRecord, - AuctionParams, - DNSRecord, - DNSRecordAttributes, - DeployerRecord, - EnvironmentVariables, - GitPushEventPayload -} from './types' -import { getRepoDetails } from './utils' - -const log = debug('snowball:service') - -const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository' - -// Define a constant for an hour in milliseconds -const HOUR = 1000 * 60 * 60 - -interface Config { - gitHubConfig: GitHubConfig - registryConfig: RegistryConfig -} - -export class Service { - private db: Database - private oauthApp: OAuthApp - private laconicRegistry: Registry - private config: Config - - private deployRecordCheckTimeout?: NodeJS.Timeout - private auctionStatusCheckTimeout?: NodeJS.Timeout - - constructor(config: Config, db: Database, app: OAuthApp, registry: Registry) { - this.db = db - this.oauthApp = app - this.laconicRegistry = registry - this.config = config - this.init() - } - - /** - * Initialize services - */ - init(): void { - // Start check for ApplicationDeploymentRecords asynchronously - this.checkDeployRecordsAndUpdate() - // Start check for ApplicationDeploymentRemovalRecords asynchronously - this.checkDeploymentRemovalRecordsAndUpdate() - // Start check for Deployment Auctions asynchronously - this.checkAuctionStatus() - } - - /** - * Destroy services - */ - destroy(): void { - clearTimeout(this.deployRecordCheckTimeout) - clearTimeout(this.auctionStatusCheckTimeout) - } - - /** - * Checks for ApplicationDeploymentRecord and update corresponding deployments - * Continues check in loop after a delay of registryConfig.fetchDeploymentRecordDelay - */ - async checkDeployRecordsAndUpdate(): Promise { - // Fetch deployments in building state - const deployments = await this.db.getDeployments({ - where: { - status: DeploymentStatus.Building - } - }) - - if (deployments.length) { - log( - `Found ${deployments.length} deployments in ${DeploymentStatus.Building} state` - ) - - // Calculate a timestamp for one hour ago - const anHourAgo = Date.now() - HOUR - - // Filter out deployments started more than an hour ago and mark them as Error - const oldDeploymentsToUpdate = deployments - .filter((deployment) => Number(deployment.updatedAt) < anHourAgo) - .map((deployment) => { - return this.db.updateDeploymentById(deployment.id, { - status: DeploymentStatus.Error, - isCurrent: false - }) - }) - - // If there are old deployments to update, log and perform the updates - if (oldDeploymentsToUpdate.length > 0) { - log( - `Cleaning up ${oldDeploymentsToUpdate.length} deployments stuck in ${DeploymentStatus.Building} state for over an hour` - ) - await Promise.all(oldDeploymentsToUpdate) - } - - // Fetch ApplicationDeploymentRecord for deployments - const records = - await this.laconicRegistry.getDeploymentRecords(deployments) - log(`Found ${records.length} ApplicationDeploymentRecords`) - - // Update deployments for which ApplicationDeploymentRecords were returned - if (records.length) { - await this.updateDeploymentsWithRecordData(records) - } - } - - this.deployRecordCheckTimeout = setTimeout(() => { - this.checkDeployRecordsAndUpdate() - }, this.config.registryConfig.fetchDeploymentRecordDelay) - } - - /** - * Checks for ApplicationDeploymentRemovalRecord and remove corresponding deployments - * Continues check in loop after a delay of registryConfig.fetchDeploymentRecordDelay - */ - async checkDeploymentRemovalRecordsAndUpdate(): Promise { - // Fetch deployments in deleting state - const deployments = await this.db.getDeployments({ - where: { - status: DeploymentStatus.Deleting - } - }) - - if (deployments.length) { - log( - `Found ${deployments.length} deployments in ${DeploymentStatus.Deleting} state` - ) - - // Fetch ApplicationDeploymentRemovalRecords for deployments - const records = - await this.laconicRegistry.getDeploymentRemovalRecords(deployments) - log(`Found ${records.length} ApplicationDeploymentRemovalRecords`) - - // Update deployments for which ApplicationDeploymentRemovalRecords were returned - if (records.length) { - await this.deleteDeploymentsWithRecordData(records, deployments) - } - } - - this.deployRecordCheckTimeout = setTimeout(() => { - this.checkDeploymentRemovalRecordsAndUpdate() - }, this.config.registryConfig.fetchDeploymentRecordDelay) - } - - /** - * Update deployments with ApplicationDeploymentRecord data - * Deployments that are completed but not updated in DB - */ - async updateDeploymentsWithRecordData( - records: AppDeploymentRecord[] - ): Promise { - // Fetch the deployments to be updated using deployment requestId - const deployments = await this.db.getDeployments({ - where: records.map((record) => ({ - applicationDeploymentRequestId: record.attributes.request - })), - relations: { - deployer: true, - project: true - }, - order: { - createdAt: 'DESC' - } - }) - - const recordToDeploymentsMap = deployments.reduce( - (acc: { [key: string]: Deployment }, deployment) => { - if (deployment.applicationDeploymentRequestId) { - acc[deployment.applicationDeploymentRequestId] = deployment - } - return acc - }, - {} - ) - - // Update deployment data for ApplicationDeploymentRecords - const deploymentUpdatePromises = records.map(async (record) => { - const deployment = recordToDeploymentsMap[record.attributes.request] - - if (!deployment.project) { - log(`Project ${deployment.projectId} not found`) - return - } - - const registryRecord = await this.laconicRegistry.getRecordById( - record.attributes.dns - ) - - if (!registryRecord) { - log(`DNS record not found for deployment ${deployment.id}`) - return - } - - const dnsRecord = registryRecord as DNSRecord - - const dnsRecordData: DNSRecordAttributes = { - name: dnsRecord.attributes.name, - request: dnsRecord.attributes.request, - resourceType: dnsRecord.attributes.resource_type, - value: dnsRecord.attributes.value, - version: dnsRecord.attributes.version - } - - deployment.applicationDeploymentRecordId = record.id - deployment.applicationDeploymentRecordData = record.attributes - deployment.url = record.attributes.url - deployment.status = DeploymentStatus.Ready - deployment.isCurrent = deployment.environment === Environment.Production - deployment.dnsRecordData = dnsRecordData - - if (deployment.isCanonical) { - const previousCanonicalDeployment = await this.db.getDeployment({ - where: { - projectId: deployment.project.id, - deployer: deployment.deployer, - isCanonical: true, - isCurrent: true - }, - relations: { - project: true, - deployer: true - } - }) - - if (previousCanonicalDeployment) { - // Send removal request for the previous canonical deployment and delete DB entry - if (previousCanonicalDeployment.url !== deployment.url) { - await this.laconicRegistry.createApplicationDeploymentRemovalRequest( - { - deploymentId: - previousCanonicalDeployment.applicationDeploymentRecordId!, - deployerLrn: previousCanonicalDeployment.deployer.deployerLrn, - auctionId: previousCanonicalDeployment.project.auctionId, - payment: previousCanonicalDeployment.project.txHash - } - ) - } - - await this.db.deleteDeploymentById(previousCanonicalDeployment.id) - } - } - - await this.db.updateDeploymentById(deployment.id, deployment) - - // Release deployer funds on successful deployment - if (!deployment.project.fundsReleased) { - const fundsReleased = await this.releaseDeployerFundsByProjectId( - deployment.projectId - ) - - // Return remaining amount to owner - await this.returnUserFundsByProjectId(deployment.projectId, true) - - await this.db.updateProjectById(deployment.projectId, { - fundsReleased - }) - } - - log( - `Updated deployment ${deployment.id} with URL ${record.attributes.url}` - ) - }) - - await Promise.all(deploymentUpdatePromises) - - // Get deployments that are in production environment - const prodDeployments = Object.values(recordToDeploymentsMap).filter( - (deployment) => deployment.isCurrent - ) - // Set the isCurrent state to false for the old deployments - for (const deployment of prodDeployments) { - const projectDeployments = await this.db.getDeploymentsByProjectId( - deployment.projectId - ) - - const oldDeployments = projectDeployments.filter( - (projectDeployment) => - projectDeployment.deployer.deployerLrn === - deployment.deployer.deployerLrn && - projectDeployment.id !== deployment.id && - projectDeployment.isCanonical === deployment.isCanonical - ) - for (const oldDeployment of oldDeployments) { - await this.db.updateDeployment( - { id: oldDeployment.id }, - { isCurrent: false } - ) - } - } - } - - /** - * Delete deployments with ApplicationDeploymentRemovalRecord data - */ - async deleteDeploymentsWithRecordData( - records: AppDeploymentRemovalRecord[], - deployments: Deployment[] - ): Promise { - const removedApplicationDeploymentRecordIds = records.map( - (record) => record.attributes.deployment - ) - - // Get removed deployments for ApplicationDeploymentRecords - const removedDeployments = deployments.filter((deployment) => - removedApplicationDeploymentRecordIds.includes( - deployment.applicationDeploymentRecordId! - ) - ) - - const recordToDeploymentsMap = removedDeployments.reduce( - (acc: { [key: string]: Deployment }, deployment) => { - if (deployment.applicationDeploymentRecordId) { - acc[deployment.applicationDeploymentRecordId] = deployment - } - return acc - }, - {} - ) - - // Update deployment data for ApplicationDeploymentRecords and delete - const deploymentUpdatePromises = records.map(async (record) => { - const deployment = recordToDeploymentsMap[record.attributes.deployment] - - await this.db.updateDeploymentById(deployment.id, { - applicationDeploymentRemovalRecordId: record.id, - applicationDeploymentRemovalRecordData: record.attributes - }) - - log( - `Updated deployment ${deployment.id} with ApplicationDeploymentRemovalRecord ${record.id}` - ) - - await this.db.deleteDeploymentById(deployment.id) - }) - - await Promise.all(deploymentUpdatePromises) - } - - /** - * Checks the status for all ongoing auctions - * Calls the createDeploymentFromAuction method for deployments with completed auctions - */ - async checkAuctionStatus(): Promise { - const projects = await this.db.allProjectsWithoutDeployments() - - const validAuctionIds = projects - .map((project) => project.auctionId) - .filter((id): id is string => Boolean(id)) - const completedAuctionIds = - await this.laconicRegistry.getCompletedAuctionIds(validAuctionIds) - - const projectsToBedeployed = projects.filter( - (project) => - project.auctionId && completedAuctionIds.includes(project.auctionId) - ) - - for (const project of projectsToBedeployed) { - const deployerRecords = - await this.laconicRegistry.getAuctionWinningDeployerRecords( - project!.auctionId! - ) - - if (!deployerRecords) { - log(`No winning deployer for auction ${project!.auctionId}`) - - // Return all funds to the owner - await this.returnUserFundsByProjectId(project.id, false) - } else { - const deployers = - await this.saveDeployersByDeployerRecords(deployerRecords) - for (const deployer of deployers) { - log(`Creating deployment for deployer ${deployer.deployerLrn}`) - await this.createDeploymentFromAuction(project, deployer) - // Update project with deployer - await this.updateProjectWithDeployer(project.id, deployer) - } - } - } - - this.auctionStatusCheckTimeout = setTimeout(() => { - this.checkAuctionStatus() - }, this.config.registryConfig.checkAuctionStatusDelay) - } - - async getUser(userId: string): Promise { - return this.db.getUser({ - where: { - id: userId - } - }) - } - - async getUserByEmail(email: string): Promise { - return await this.db.getUser({ - where: { - email - } - }) - } - - async getUserBySubOrgId(subOrgId: string): Promise { - return await this.db.getUser({ - where: { - subOrgId - } - }) - } - - async getUserByEthAddress(ethAddress: string): Promise { - return await this.db.getUser({ - where: { - ethAddress - } - }) - } - - async createUser(params: { - name?: string - email: string - subOrgId: string - ethAddress: string - turnkeyWalletId: string - }): Promise { - const [org] = await this.db.getOrganizations({}) - assert(org, 'No organizations exists in database') - - // Create user with new address - const user = await this.db.addUser({ - email: params.email, - name: params.name, - subOrgId: params.subOrgId, - ethAddress: params.ethAddress, - isVerified: true, - turnkeyWalletId: params.turnkeyWalletId - }) - - await this.db.addUserOrganization({ - member: user, - organization: org, - role: Role.Owner - }) - - return user - } - - async getOctokit(userId: string): Promise { - const user = await this.db.getUser({ where: { id: userId } }) - assert( - user?.gitHubToken, - 'User needs to be authenticated with GitHub token' - ) - - return new Octokit({ auth: user.gitHubToken }) - } - - async getOrganizationsByUserId(user: User): Promise { - const dbOrganizations = await this.db.getOrganizationsByUserId(user.id) - return dbOrganizations - } - - async getProjectById(user: User, projectId: string): Promise { - const dbProject = await this.db.getProjectById(projectId) - - if (dbProject && dbProject.owner.id !== user.id) { - return null - } - - return dbProject - } - - async getProjectsInOrganization( - user: User, - organizationSlug: string - ): Promise { - const dbProjects = await this.db.getProjectsInOrganization( - user.id, - organizationSlug - ) - return dbProjects - } - - async getNonCanonicalDeploymentsByProjectId( - projectId: string - ): Promise { - const nonCanonicalDeployments = - await this.db.getNonCanonicalDeploymentsByProjectId(projectId) - return nonCanonicalDeployments - } - - async getLatestDNSRecordByProjectId( - projectId: string - ): Promise { - const dnsRecord = await this.db.getLatestDNSRecordByProjectId(projectId) - return dnsRecord - } - - async getEnvironmentVariablesByProjectId( - projectId: string - ): Promise { - const dbEnvironmentVariables = - await this.db.getEnvironmentVariablesByProjectId(projectId) - return dbEnvironmentVariables - } - - async getProjectMembersByProjectId( - projectId: string - ): Promise { - const dbProjectMembers = - await this.db.getProjectMembersByProjectId(projectId) - return dbProjectMembers - } - - async searchProjects(user: User, searchText: string): Promise { - const dbProjects = await this.db.getProjectsBySearchText( - user.id, - searchText - ) - return dbProjects - } - - async getDomainsByProjectId( - projectId: string, - filter?: FindOptionsWhere - ): Promise { - const dbDomains = await this.db.getDomainsByProjectId(projectId, filter) - return dbDomains - } - - async updateProjectMember( - projectMemberId: string, - data: { permissions: Permission[] } - ): Promise { - return this.db.updateProjectMemberById(projectMemberId, data) - } - - async addProjectMember( - projectId: string, - data: { - email: string - permissions: Permission[] - } - ): Promise { - // TODO: Send invitation - let user = await this.db.getUser({ - where: { - email: data.email - } - }) - - if (!user) { - user = await this.db.addUser({ - email: data.email - }) - } - - const newProjectMember = await this.db.addProjectMember({ - project: { - id: projectId - }, - permissions: data.permissions, - isPending: true, - member: { - id: user.id - } - }) - - return newProjectMember - } - - async removeProjectMember( - user: User, - projectMemberId: string - ): Promise { - const member = await this.db.getProjectMemberById(projectMemberId) - - if (String(member.member.id) === user.id) { - throw new Error('Invalid operation: cannot remove self') - } - - const memberProject = member.project - assert(memberProject) - - if (String(user.id) === String(memberProject.owner.id)) { - return this.db.removeProjectMemberById(projectMemberId) - } - - throw new Error('Invalid operation: not authorized') - } - - async addEnvironmentVariables( - projectId: string, - data: { environments: string[]; key: string; value: string }[] - ): Promise { - const formattedEnvironmentVariables = data.flatMap( - (environmentVariable) => { - return environmentVariable.environments.map((environment) => { - return { - key: environmentVariable.key, - value: environmentVariable.value, - environment: environment as Environment, - project: Object.assign(new Project(), { - id: projectId - }) - } - }) - } - ) - - const savedEnvironmentVariables = await this.db.addEnvironmentVariables( - formattedEnvironmentVariables - ) - return savedEnvironmentVariables - } - - async updateEnvironmentVariable( - environmentVariableId: string, - data: DeepPartial - ): Promise { - return this.db.updateEnvironmentVariable(environmentVariableId, data) - } - - async removeEnvironmentVariable( - environmentVariableId: string - ): Promise { - return this.db.deleteEnvironmentVariable(environmentVariableId) - } - - async updateDeploymentToProd( - user: User, - deploymentId: string - ): Promise { - const oldDeployment = await this.db.getDeployment({ - where: { id: deploymentId }, - relations: { - project: true, - deployer: true - } - }) - - if (!oldDeployment) { - throw new Error('Deployment does not exist') - } - - const octokit = await this.getOctokit(user.id) - - const newDeployment = await this.createDeployment(user.id, octokit, { - project: oldDeployment.project, - branch: oldDeployment.branch, - environment: Environment.Production, - commitHash: oldDeployment.commitHash, - commitMessage: oldDeployment.commitMessage, - deployer: oldDeployment.deployer - }) - - return newDeployment - } - - async createDeployment( - userId: string, - octokit: Octokit, - data: DeepPartial, - deployerLrn?: string - ): Promise { - assert(data.project?.repository, 'Project repository not found') - log( - `Creating deployment in project ${data.project.name} from branch ${data.branch}` - ) - - // TODO: Set environment variables for each deployment (environment variables can`t be set in application record) - const { applicationRecordId, applicationRecordData } = - await this.laconicRegistry.createApplicationRecord({ - octokit, - repository: data.project.repository, - appType: data.project!.template!, - commitHash: data.commitHash! - }) - - let deployer: Deployer | undefined - if (deployerLrn) { - const found = await this.db.getDeployerByLRN(deployerLrn) - deployer = found || undefined - } else { - deployer = data.deployer as Deployer - } - - const deployment = await this.createDeploymentFromData( - userId, - data, - deployer!.deployerLrn!, - applicationRecordId, - applicationRecordData, - false - ) - - const address = await this.getAddress() - const { repo, repoUrl } = await getRepoDetails( - octokit, - data.project.repository, - data.commitHash - ) - const environmentVariablesObj = await this.getEnvVariables( - data.project!.id! - ) - - // To set project DNS - if (data.environment === Environment.Production) { - const canonicalDeployment = await this.createDeploymentFromData( - userId, - data, - deployer!.deployerLrn!, - applicationRecordId, - applicationRecordData, - true - ) - // If a custom domain is present then use that as the DNS in the deployment request - const customDomain = await this.db.getOldestDomainByProjectId( - data.project!.id! - ) - - // On deleting deployment later, project canonical deployment is also deleted - // So publish project canonical deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later - const { - applicationDeploymentRequestData, - applicationDeploymentRequestId - } = await this.laconicRegistry.createApplicationDeploymentRequest({ - deployment: canonicalDeployment, - appName: repo, - repository: repoUrl, - environmentVariables: environmentVariablesObj, - dns: customDomain?.name ?? `${canonicalDeployment.project.name}`, - lrn: deployer!.deployerLrn!, - apiUrl: deployer!.deployerApiUrl!, - payment: data.project.txHash, - auctionId: data.project.auctionId, - requesterAddress: address, - publicKey: deployer!.publicKey! - }) - - await this.db.updateDeploymentById(canonicalDeployment.id, { - applicationDeploymentRequestId, - applicationDeploymentRequestData - }) - } - - const { applicationDeploymentRequestId, applicationDeploymentRequestData } = - await this.laconicRegistry.createApplicationDeploymentRequest({ - deployment: deployment, - appName: repo, - repository: repoUrl, - lrn: deployer!.deployerLrn!, - apiUrl: deployer!.deployerApiUrl!, - environmentVariables: environmentVariablesObj, - dns: `${deployment.project.name}-${deployment.id}`, - payment: data.project.txHash, - auctionId: data.project.auctionId, - requesterAddress: address, - publicKey: deployer!.publicKey! - }) - - await this.db.updateDeploymentById(deployment.id, { - applicationDeploymentRequestId, - applicationDeploymentRequestData - }) - - return deployment - } - - async createDeploymentFromAuction( - project: DeepPartial, - deployer: Deployer - ): Promise { - const octokit = await this.getOctokit(project.ownerId!) - const [owner, repo] = project.repository!.split('/') - - const repoUrl = ( - await octokit.rest.repos.get({ - owner, - repo - }) - ).data.html_url - - const { - data: [latestCommit] - } = await octokit.rest.repos.listCommits({ - owner, - repo, - sha: project.prodBranch, - per_page: 1 - }) - - const lrn = this.laconicRegistry.getLrn(repo) - const [record] = await this.laconicRegistry.getRecordsByName(lrn) - const applicationRecordId = record.id - const applicationRecordData = record.attributes - - const deployerLrn = deployer!.deployerLrn - - // Create deployment with prod branch and latest commit - const deploymentData = { - project, - branch: project.prodBranch, - environment: Environment.Production, - domain: null, - commitHash: latestCommit.sha, - commitMessage: latestCommit.commit.message - } - - const deployment = await this.createDeploymentFromData( - project.ownerId!, - deploymentData, - deployerLrn, - applicationRecordId, - applicationRecordData, - false - ) - const address = await this.getAddress() - - const environmentVariablesObj = await this.getEnvVariables(project!.id!) - // To set project DNS - if (deploymentData.environment === Environment.Production) { - const canonicalDeployment = await this.createDeploymentFromData( - project.ownerId!, - deploymentData, - deployerLrn, - applicationRecordId, - applicationRecordData, - true - ) - // If a custom domain is present then use that as the DNS in the deployment request - const customDomain = await this.db.getOldestDomainByProjectId( - project!.id! - ) - - // On deleting deployment later, project canonical deployment is also deleted - // So publish project canonical deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later - const { - applicationDeploymentRequestId, - applicationDeploymentRequestData - } = await this.laconicRegistry.createApplicationDeploymentRequest({ - deployment: canonicalDeployment, - appName: repo, - repository: repoUrl, - environmentVariables: environmentVariablesObj, - dns: customDomain?.name ?? `${canonicalDeployment.project.name}`, - auctionId: project.auctionId!, - lrn: deployerLrn, - apiUrl: deployer!.deployerApiUrl!, - requesterAddress: address, - publicKey: deployer!.publicKey! - }) - - await this.db.updateDeploymentById(canonicalDeployment.id, { - applicationDeploymentRequestId, - applicationDeploymentRequestData - }) - } - - const { applicationDeploymentRequestId, applicationDeploymentRequestData } = - // Create requests for all the deployers - await this.laconicRegistry.createApplicationDeploymentRequest({ - deployment: deployment, - appName: repo, - repository: repoUrl, - auctionId: project.auctionId!, - lrn: deployerLrn, - apiUrl: deployer!.deployerApiUrl!, - environmentVariables: environmentVariablesObj, - dns: `${deployment.project.name}-${deployment.id}`, - requesterAddress: address, - publicKey: deployer!.publicKey! - }) - - await this.db.updateDeploymentById(deployment.id, { - applicationDeploymentRequestId, - applicationDeploymentRequestData - }) - - return deployment - } - - async createDeploymentFromData( - userId: string, - data: DeepPartial, - deployerLrn: string, - applicationRecordId: string, - applicationRecordData: ApplicationRecord, - isCanonical: boolean - ): Promise { - const newDeployment = await this.db.addDeployment({ - project: data.project, - branch: data.branch, - commitHash: data.commitHash, - commitMessage: data.commitMessage, - environment: data.environment, - status: DeploymentStatus.Building, - applicationRecordId, - applicationRecordData, - createdBy: Object.assign(new User(), { - id: userId - }), - deployer: Object.assign(new Deployer(), { - deployerLrn - }), - isCanonical - }) - - log(`Created deployment ${newDeployment.id}`) - - return newDeployment - } - - async updateProjectWithDeployer( - projectId: string, - deployer: Deployer - ): Promise { - const deploymentProject = await this.db.getProjects({ - where: { id: projectId }, - relations: ['deployers'] - }) - - if (!deploymentProject[0].deployers) { - deploymentProject[0].deployers = [] - } - - deploymentProject[0].deployers.push(deployer) - - await this.db.saveProject(deploymentProject[0]) - - return deployer - } - - async addProjectFromTemplate( - user: User, - organizationSlug: string, - data: AddProjectFromTemplateInput, - lrn?: string, - auctionParams?: AuctionParams, - environmentVariables?: EnvironmentVariables[] - ): Promise { - try { - const octokit = await this.getOctokit(user.id) - - const gitRepo = await octokit?.rest.repos.createUsingTemplate({ - template_owner: data.templateOwner, - template_repo: data.templateRepo, - owner: data.owner, - name: data.name, - include_all_branches: false, - private: data.isPrivate - }) - - if (!gitRepo) { - throw new Error('Failed to create repository from template') - } - - const createdTemplateRepo = await octokit.rest.repos.get({ - owner: data.owner, - repo: data.name - }) - - const prodBranch = createdTemplateRepo.data.default_branch ?? 'main' - - const project = await this.addProject( - user, - organizationSlug, - { - name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`, - prodBranch, - repository: gitRepo.data.full_name, - // TODO: Set selected template - template: 'webapp', - paymentAddress: data.paymentAddress, - txHash: data.txHash - }, - lrn, - auctionParams, - environmentVariables - ) - - if (!project || !project.id) { - throw new Error('Failed to create project from template') - } - - return project - } catch (error) { - console.error('Error creating project from template:', error) - throw error - } - } - - async addProject( - user: User, - organizationSlug: string, - data: DeepPartial, - lrn?: string, - auctionParams?: AuctionParams, - environmentVariables?: EnvironmentVariables[] - ): Promise { - const organization = await this.db.getOrganization({ - where: { - slug: organizationSlug - } - }) - - if (!organization) { - throw new Error('Organization does not exist') - } - - const project = await this.db.addProject(user, organization.id, data) - - if (environmentVariables) { - await this.addEnvironmentVariables(project.id, environmentVariables) - } - - const octokit = await this.getOctokit(user.id) - const [owner, repo] = project.repository.split('/') - - const { - data: [latestCommit] - } = await octokit.rest.repos.listCommits({ - owner, - repo, - sha: project.prodBranch, - per_page: 1 - }) - - if (auctionParams) { - // Create deployment with prod branch and latest commit - const deploymentData = { - project, - branch: project.prodBranch, - environment: Environment.Production, - domain: null, - commitHash: latestCommit.sha, - commitMessage: latestCommit.commit.message - } - const { applicationDeploymentAuctionId } = - await this.laconicRegistry.createApplicationDeploymentAuction( - repo, - octokit, - auctionParams!, - deploymentData - ) - await this.updateProject(project.id, { - auctionId: applicationDeploymentAuctionId - }) - } else { - const deployer = await this.db.getDeployerByLRN(lrn!) - - if (!deployer) { - log('Invalid deployer LRN') - return - } - - if (deployer.minimumPayment && project.txHash) { - const amountToBePaid = deployer?.minimumPayment - .replace(/\D/g, '') - .toString() - - const txResponse = await this.laconicRegistry.sendTokensToAccount( - deployer?.paymentAddress!, - amountToBePaid - ) - - const txHash = txResponse.transactionHash - if (txHash) { - await this.updateProject(project.id, { txHash }) - project.txHash = txHash - log('Funds transferrend to deployer') - } - } - - const deploymentData = { - project, - branch: project.prodBranch, - environment: Environment.Production, - domain: null, - commitHash: latestCommit.sha, - commitMessage: latestCommit.commit.message, - deployer - } - - const newDeployment = await this.createDeployment( - user.id, - octokit, - deploymentData - ) - // Update project with deployer - await this.updateProjectWithDeployer( - newDeployment.projectId, - newDeployment.deployer - ) - } - - await this.createRepoHook(octokit, project) - - return project - } - - async createRepoHook(octokit: Octokit, project: Project): Promise { - try { - const [owner, repo] = project.repository.split('/') - await octokit.rest.repos.createWebhook({ - owner, - repo, - config: { - url: new URL( - 'api/github/webhook', - this.config.gitHubConfig.webhookUrl - ).href, - content_type: 'json' - }, - events: ['push'] - }) - } catch (err) { - // https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes - if ( - !( - err instanceof RequestError && - err.status === 422 && - (err.response?.data as any).errors.some( - (err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR - ) - ) - ) { - throw err - } - - log(GITHUB_UNIQUE_WEBHOOK_ERROR) - } - } - - async handleGitHubPush(data: GitPushEventPayload): Promise { - const { repository, ref, head_commit: headCommit, deleted } = data - - if (deleted) { - log(`Branch ${ref} deleted for project ${repository.full_name}`) - return - } - - log( - `Handling GitHub push event from repository: ${repository.full_name}, branch: ${ref}` - ) - const projects = await this.db.getProjects({ - where: { repository: repository.full_name }, - relations: { - deployers: true - } - }) - - if (!projects.length) { - log(`No projects found for repository ${repository.full_name}`) - } - - // The `ref` property contains the full reference, including the branch name - // For example, "refs/heads/main" or "refs/heads/feature-branch" - const branch = ref.split('/').pop() - - for await (const project of projects) { - const octokit = await this.getOctokit(project.ownerId) - - const deployers = project.deployers - if (!deployers) { - log(`No deployer present for project ${project.id}`) - return - } - - for (const deployer of deployers) { - // Create deployment with branch and latest commit in GitHub data - await this.createDeployment(project.ownerId, octokit, { - project, - branch, - environment: - project.prodBranch === branch - ? Environment.Production - : Environment.Preview, - commitHash: headCommit.id, - commitMessage: headCommit.message, - deployer: deployer - }) - } - } - } - - async updateProject( - projectId: string, - data: DeepPartial - ): Promise { - return this.db.updateProjectById(projectId, data) - } - - async deleteProject(projectId: string): Promise { - // TODO: Remove GitHub repo hook - return this.db.deleteProjectById(projectId) - } - - async deleteDomain(domainId: string): Promise { - const domainsRedirectedFrom = await this.db.getDomains({ - where: { - redirectToId: domainId - } - }) - - if (domainsRedirectedFrom.length > 0) { - throw new Error( - 'Cannot delete domain since it has redirects from other domains' - ) - } - - return this.db.deleteDomainById(domainId) - } - - async redeployToProd(user: User, deploymentId: string): Promise { - const oldDeployment = await this.db.getDeployment({ - relations: { - project: true, - deployer: true, - createdBy: true - }, - where: { - id: deploymentId - } - }) - - if (oldDeployment === null) { - throw new Error('Deployment not found') - } - - const octokit = await this.getOctokit(user.id) - - let newDeployment: Deployment - - if (oldDeployment.project.auctionId) { - newDeployment = await this.createDeploymentFromAuction( - oldDeployment.project, - oldDeployment.deployer - ) - } else { - newDeployment = await this.createDeployment(user.id, octokit, { - project: oldDeployment.project, - // TODO: Put isCurrent field in project - branch: oldDeployment.branch, - environment: Environment.Production, - commitHash: oldDeployment.commitHash, - commitMessage: oldDeployment.commitMessage, - deployer: oldDeployment.deployer - }) - } - - return newDeployment - } - - async rollbackDeployment( - projectId: string, - deploymentId: string - ): Promise { - // TODO: Implement transactions - const oldCurrentDeployment = await this.db.getDeployment({ - relations: { - project: true, - deployer: true - }, - where: { - project: { - id: projectId - }, - isCurrent: true, - isCanonical: false - } - }) - - if (!oldCurrentDeployment) { - throw new Error('Current deployment does not exist') - } - - const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById( - oldCurrentDeployment.id, - { isCurrent: false } - ) - - const newCurrentDeploymentUpdate = await this.db.updateDeploymentById( - deploymentId, - { isCurrent: true } - ) - - if (!newCurrentDeploymentUpdate || !oldCurrentDeploymentUpdate) { - return false - } - - const newCurrentDeployment = await this.db.getDeployment({ - where: { id: deploymentId }, - relations: { project: true, deployer: true } - }) - - if (!newCurrentDeployment) { - throw new Error(`Deployment with Id ${deploymentId} not found`) - } - - const applicationDeploymentRequestData = - newCurrentDeployment.applicationDeploymentRequestData - - const customDomain = await this.db.getOldestDomainByProjectId(projectId) - - if (customDomain && applicationDeploymentRequestData) { - applicationDeploymentRequestData.dns = customDomain.name - } - - // Create a canonical deployment for the new current deployment - const canonicalDeployment = await this.createDeploymentFromData( - newCurrentDeployment.project.ownerId, - newCurrentDeployment, - newCurrentDeployment.deployer!.deployerLrn!, - newCurrentDeployment.applicationRecordId, - newCurrentDeployment.applicationRecordData, - true - ) - - applicationDeploymentRequestData!.meta = JSON.stringify({ - ...JSON.parse(applicationDeploymentRequestData!.meta), - note: `Updated by Snowball @ ${DateTime.utc().toFormat( - "EEE LLL dd HH:mm:ss 'UTC' yyyy" - )}` - }) - - const result = await this.laconicRegistry.publishRecord( - applicationDeploymentRequestData - ) - - log(`Application deployment request record published: ${result.id}`) - - const updateResult = await this.db.updateDeploymentById( - canonicalDeployment.id, - { - applicationDeploymentRequestId: result.id, - applicationDeploymentRequestData - } - ) - - return updateResult - } - - async deleteDeployment(deploymentId: string): Promise { - const deployment = await this.db.getDeployment({ - where: { - id: deploymentId - }, - relations: { - project: true, - deployer: true - } - }) - - if (deployment?.applicationDeploymentRecordId) { - // If deployment is current, remove deployment for project subdomain as well - if (deployment.isCurrent) { - const canonicalDeployment = await this.db.getDeployment({ - where: { - projectId: deployment.project.id, - deployer: deployment.deployer, - isCanonical: true - }, - relations: { - project: true, - deployer: true - } - }) - - // If the canonical deployment is not present then query the chain for the deployment record for backward compatibility - if (!canonicalDeployment) { - log( - `Canonical deployment for deployment with id ${deployment.id} not found, querying the chain..` - ) - const currentDeploymentURL = `https://${(deployment.project.name).toLowerCase()}.${deployment.deployer.baseDomain}` - - const deploymentRecords = - await this.laconicRegistry.getDeploymentRecordsByFilter({ - application: deployment.applicationRecordId, - url: currentDeploymentURL - }) - - if (!deploymentRecords.length) { - log( - `No ApplicationDeploymentRecord found for URL ${currentDeploymentURL} and ApplicationDeploymentRecord id ${deployment.applicationDeploymentRecordId}` - ) - - return false - } - - // Multiple records are fetched, take the latest record - const latestRecord = deploymentRecords.sort( - (a, b) => - new Date(b.createTime).getTime() - - new Date(a.createTime).getTime() - )[0] - - await this.laconicRegistry.createApplicationDeploymentRemovalRequest({ - deploymentId: latestRecord.id, - deployerLrn: deployment.deployer.deployerLrn, - auctionId: deployment.project.auctionId, - payment: deployment.project.txHash - }) - } else { - // If canonical deployment is found in the DB, then send the removal request with that deployment record Id - const result = - await this.laconicRegistry.createApplicationDeploymentRemovalRequest( - { - deploymentId: - canonicalDeployment.applicationDeploymentRecordId!, - deployerLrn: canonicalDeployment.deployer.deployerLrn, - auctionId: canonicalDeployment.project.auctionId, - payment: canonicalDeployment.project.txHash - } - ) - - await this.db.updateDeploymentById(canonicalDeployment.id, { - status: DeploymentStatus.Deleting, - applicationDeploymentRemovalRequestId: - result.applicationDeploymentRemovalRequestId, - applicationDeploymentRemovalRequestData: - result.applicationDeploymentRemovalRequestData - }) - } - } - - const result = - await this.laconicRegistry.createApplicationDeploymentRemovalRequest({ - deploymentId: deployment.applicationDeploymentRecordId, - deployerLrn: deployment.deployer.deployerLrn, - auctionId: deployment.project.auctionId, - payment: deployment.project.txHash - }) - - await this.db.updateDeploymentById(deployment.id, { - status: DeploymentStatus.Deleting, - applicationDeploymentRemovalRequestId: - result.applicationDeploymentRemovalRequestId, - applicationDeploymentRemovalRequestData: - result.applicationDeploymentRemovalRequestData - }) - - return result !== undefined || result !== null - } - - return false - } - - async addDomain( - projectId: string, - data: { name: string } - ): Promise<{ - primaryDomain: Domain - // redirectedDomain: Domain; - }> { - const currentProject = await this.db.getProjectById(projectId) - - if (currentProject === null) { - throw new Error(`Project with ${projectId} not found`) - } - - const primaryDomainDetails = { - ...data, - branch: currentProject.prodBranch, - project: currentProject - } - - const savedPrimaryDomain = await this.db.addDomain(primaryDomainDetails) - - // const domainArr = data.name.split('www.'); - - // const redirectedDomainDetails = { - // name: domainArr.length > 1 ? domainArr[1] : `www.${domainArr[0]}`, - // branch: currentProject.prodBranch, - // project: currentProject, - // redirectTo: savedPrimaryDomain, - // }; - - // const savedRedirectedDomain = await this.db.addDomain( - // redirectedDomainDetails, - // ); - - return { - primaryDomain: savedPrimaryDomain - // redirectedDomain: savedRedirectedDomain, - } - } - - async updateDomain( - domainId: string, - data: DeepPartial - ): Promise { - const domain = await this.db.getDomain({ - where: { - id: domainId - } - }) - - if (domain === null) { - throw new Error(`Error finding domain with id ${domainId}`) - } - - const newDomain = { - ...data - } - - const domainsRedirectedFrom = await this.db.getDomains({ - where: { - project: { - id: domain.projectId - }, - redirectToId: domain.id - } - }) - - // If there are domains redirecting to current domain, only branch of current domain can be updated - if (domainsRedirectedFrom.length > 0 && data.branch === domain.branch) { - throw new Error('Remove all redirects to this domain before updating') - } - - if (data.redirectToId) { - const redirectedDomain = await this.db.getDomain({ - where: { - id: data.redirectToId - } - }) - - if (redirectedDomain === null) { - throw new Error('Could not find Domain to redirect to') - } - - if (redirectedDomain.redirectToId) { - throw new Error( - 'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.' - ) - } - - newDomain.redirectTo = redirectedDomain - } - - const updateResult = await this.db.updateDomainById(domainId, newDomain) - - return updateResult - } - - async authenticateGitHub( - code: string, - user: User - ): Promise<{ token: string }> { - const { - authentication: { token } - } = await this.oauthApp.createToken({ - code - }) - - await this.db.updateUser(user, { gitHubToken: token }) - - return { token } - } - - async unauthenticateGitHub( - user: User, - data: DeepPartial - ): Promise { - return this.db.updateUser(user, data) - } - - async getEnvVariables(projectId: string): Promise<{ [key: string]: string }> { - const environmentVariables = - await this.db.getEnvironmentVariablesByProjectId(projectId, { - environment: Environment.Production - }) - - const environmentVariablesObj = environmentVariables.reduce( - (acc, env) => { - acc[env.key] = env.value - return acc - }, - {} as { [key: string]: string } - ) - - return environmentVariablesObj - } - - async getAuctionData(auctionId: string): Promise { - const auctions = await this.laconicRegistry.getAuctionData(auctionId) - return auctions[0] - } - - async releaseDeployerFundsByProjectId(projectId: string): Promise { - const project = await this.db.getProjectById(projectId) - - if (!project || !project.auctionId) { - log( - `Project ${projectId} ${!project ? 'not found' : 'does not have an auction'}` - ) - - return false - } - - const auction = await this.laconicRegistry.releaseDeployerFunds( - project.auctionId - ) - - if (auction.auction.fundsReleased) { - log(`Funds released for auction ${project.auctionId}`) - await this.db.updateProjectById(projectId, { fundsReleased: true }) - - return true - } - - log(`Error releasing funds for auction ${project.auctionId}`) - - return false - } - - async returnUserFundsByProjectId( - projectId: string, - winningDeployersPresent: boolean - ) { - const project = await this.db.getProjectById(projectId) - - if (!project || !project.auctionId) { - log( - `Project ${projectId} ${!project ? 'not found' : 'does not have an auction'}` - ) - - return false - } - - const auction = await this.getAuctionData(project.auctionId) - const totalAuctionPrice = - Number(auction.maxPrice.quantity) * auction.numProviders - - let amountToBeReturned: number - if (winningDeployersPresent) { - amountToBeReturned = - totalAuctionPrice - - auction.winnerAddresses.length * Number(auction.winnerPrice.quantity) - } else { - amountToBeReturned = totalAuctionPrice - } - - if (amountToBeReturned !== 0) { - await this.laconicRegistry.sendTokensToAccount( - project.paymentAddress, - amountToBeReturned.toString() - ) - } - } - - async getDeployers(): Promise { - const dbDeployers = await this.db.getDeployers() - - if (dbDeployers.length > 0) { - // Call asynchronously to fetch the records from the registry and update the DB - this.updateDeployersFromRegistry() - return dbDeployers - } - - // Fetch from the registry and populate empty DB - return await this.updateDeployersFromRegistry() - } - - async updateDeployersFromRegistry(): Promise { - const deployerRecords = - await this.laconicRegistry.getDeployerRecordsByFilter({}) - await this.saveDeployersByDeployerRecords(deployerRecords) - - return await this.db.getDeployers() - } - - async saveDeployersByDeployerRecords( - deployerRecords: DeployerRecord[] - ): Promise { - const deployers: Deployer[] = [] - - for (const record of deployerRecords) { - if (record.names && record.names.length > 0) { - const deployerId = record.id - const deployerLrn = record.names[0] - const deployerApiUrl = record.attributes.apiUrl - const minimumPayment = record.attributes.minimumPayment - const paymentAddress = record.attributes.paymentAddress - const publicKey = record.attributes.publicKey - const baseDomain = deployerApiUrl.substring( - deployerApiUrl.indexOf('.') + 1 - ) - - const deployerData = { - deployerLrn, - deployerId, - deployerApiUrl, - baseDomain, - minimumPayment, - paymentAddress, - publicKey - } - - // TODO: Update deployers table in a separate job - const deployer = await this.db.addDeployer(deployerData) - deployers.push(deployer) - } - } - - return deployers - } - - async getAddress(): Promise { - const account = await this.laconicRegistry.getAccount() - - return account.address - } - - async verifyTx( - txHash: string, - amountSent: string, - senderAddress: string - ): Promise { - const txResponse = await this.laconicRegistry.getTxResponse(txHash) - if (!txResponse) { - log('Transaction response not found') - return false - } - - const transfer = txResponse.events.find( - (e: any) => - e.type === 'transfer' && - e.attributes.some((a: any) => a.key === 'msg_index') - ) - if (!transfer) { - log('No transfer event found') - return false - } - - const sender = transfer.attributes.find( - (a: any) => a.key === 'sender' - )?.value - const recipient = transfer.attributes.find( - (a: any) => a.key === 'recipient' - )?.value - const amount = transfer.attributes.find( - (a: any) => a.key === 'amount' - )?.value - - const recipientAddress = await this.getAddress() - - return ( - amount === amountSent && - sender === senderAddress && - recipient === recipientAddress - ) - } -} diff --git a/apps/backend/src/turnkey-backend.ts b/apps/backend/src/turnkey-backend.ts deleted file mode 100644 index 2336bdd..0000000 --- a/apps/backend/src/turnkey-backend.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { Turnkey, type TurnkeyApiTypes } from '@turnkey/sdk-server' - -// Default path for the first Ethereum address in a new HD wallet. -// See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki, paths are in the form: -// m / purpose' / coin_type' / account' / change / address_index -// - Purpose is a constant set to 44' following the BIP43 recommendation. -// - Coin type is set to 60 (ETH) -- see https://github.com/satoshilabs/slips/blob/master/slip-0044.md -// - Account, Change, and Address Index are set to 0 -import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-server' -import type { Service } from './service' -import { getConfig } from './utils' - -type TAttestation = TurnkeyApiTypes['v1Attestation'] - -type CreateUserParams = { - userName: string - userEmail: string - challenge: string - attestation: TAttestation -} - -export async function createUser( - service: Service, - { userName, userEmail, challenge, attestation }: CreateUserParams -) { - try { - if (await service.getUserByEmail(userEmail)) { - throw new Error(`User already exists: ${userEmail}`) - } - - const config = await getConfig() - const turnkey = new Turnkey(config.turnkey) - - const apiClient = turnkey.api() - - const walletName = 'Default ETH Wallet' - - const createSubOrgResponse = await apiClient.createSubOrganization({ - subOrganizationName: `Default SubOrg for ${userEmail}`, - rootQuorumThreshold: 1, - rootUsers: [ - { - userName, - userEmail, - apiKeys: [], - authenticators: [ - { - authenticatorName: 'Passkey', - challenge, - attestation - } - ] - } - ], - wallet: { - walletName: walletName, - accounts: DEFAULT_ETHEREUM_ACCOUNTS - } - }) - - const subOrgId = refineNonNull(createSubOrgResponse.subOrganizationId) - const wallet = refineNonNull(createSubOrgResponse.wallet) - - const result = { - id: wallet.walletId, - address: wallet.addresses[0], - subOrgId: subOrgId - } - console.log('Turnkey success', result) - - const user = await service.createUser({ - name: userName, - email: userEmail, - subOrgId, - ethAddress: wallet.addresses[0], - turnkeyWalletId: wallet.walletId - }) - console.log('New user', user) - - return user - } catch (e) { - console.error('Failed to create user:', e) - throw e - } -} - -export async function authenticateUser( - service: Service, - signedWhoamiRequest: { - url: string - body: any - stamp: { - stampHeaderName: string - stampHeaderValue: string - } - } -) { - try { - const tkRes = await fetch(signedWhoamiRequest.url, { - method: 'POST', - body: signedWhoamiRequest.body, - headers: { - [signedWhoamiRequest.stamp.stampHeaderName]: - signedWhoamiRequest.stamp.stampHeaderValue - } - }) - console.log('AUTH RESULT', tkRes.status) - if (tkRes.status !== 200) { - console.log(await tkRes.text()) - return null - } - const orgId = (await tkRes.json()).organizationId - const user = await service.getUserBySubOrgId(orgId) - return user - } catch (e) { - console.error('Failed to authenticate:', e) - throw e - } -} - -function refineNonNull( - input: T | null | undefined, - errorMessage?: string -): T { - if (input == null) { - throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`) - } - - return input -} diff --git a/apps/backend/src/types.ts b/apps/backend/src/types.ts deleted file mode 100644 index 82f6111..0000000 --- a/apps/backend/src/types.ts +++ /dev/null @@ -1,124 +0,0 @@ -export interface PackageJSON { - name: string - version: string - author?: string - description?: string - homepage?: string - license?: string - repository?: string -} - -export interface GitRepositoryDetails { - id: number - name: string - full_name: string - visibility?: string - updated_at?: string | null - default_branch?: string -} - -export interface GitPushEventPayload { - repository: GitRepositoryDetails - ref: string - head_commit: { - id: string - message: string - } - deleted: boolean -} - -export interface AppDeploymentRecordAttributes { - application: string - auction: string - deployer: string - dns: string - meta: string - name: string - request: string - type: string - url: string - version: string -} - -export interface DNSRecordAttributes { - name: string - value: string - request: string - resourceType: string - version: string -} - -export interface RegistryDNSRecordAttributes { - name: string - value: string - request: string - resource_type: string - version: string -} - -export interface AppDeploymentRemovalRecordAttributes { - deployment: string - request: string - type: 'ApplicationDeploymentRemovalRecord' - version: string -} - -export interface RegistryRecord { - id: string - names: string[] | null - owners: string[] - bondId: string - createTime: string - expiryTime: string -} - -export interface AppDeploymentRecord extends RegistryRecord { - attributes: AppDeploymentRecordAttributes -} - -export interface AppDeploymentRemovalRecord extends RegistryRecord { - attributes: AppDeploymentRemovalRecordAttributes -} - -export interface DNSRecord extends RegistryRecord { - attributes: RegistryDNSRecordAttributes -} - -export interface AddProjectFromTemplateInput { - templateOwner: string - templateRepo: string - owner: string - name: string - isPrivate: boolean - paymentAddress: string - txHash: string -} - -export interface AuctionParams { - maxPrice: string - numProviders: number -} - -export interface EnvironmentVariables { - environments: string[] - key: string - value: string -} - -export interface DeployerRecord { - id: string - names: string[] - owners: string[] - bondId: string - createTime: string - expiryTime: string - attributes: { - apiUrl: string - minimumPayment: string | null - name: string - paymentAddress: string - publicKey: string - type: string - version: string - } -} diff --git a/apps/backend/src/utils.ts b/apps/backend/src/utils.ts deleted file mode 100644 index 072ba44..0000000 --- a/apps/backend/src/utils.ts +++ /dev/null @@ -1,160 +0,0 @@ -import debug from 'debug' -import fs from 'fs-extra' -import assert from 'node:assert' -import path from 'node:path' -import type { Octokit } from 'octokit' -import toml from 'toml' -import type { - DataSource, - DeepPartial, - EntityTarget, - ObjectLiteral -} from 'typeorm' - -import type { Config } from './config' - -interface PackageJSON { - name: string - description?: string - homepage?: string - license?: string - author?: string | { [key: string]: unknown } - version?: string - [key: string]: unknown -} - -const log = debug('snowball:utils') - -export async function getConfig() { - return await _getConfig( - path.join(__dirname, '../environments/local.toml') - ) -} - -const _getConfig = async ( - configFile: string -): Promise => { - const fileExists = await fs.pathExists(configFile) - if (!fileExists) { - throw new Error(`Config file not found: ${configFile}`) - } - - const config = toml.parse(await fs.readFile(configFile, 'utf8')) - log('config', JSON.stringify(config, null, 2)) - - return config -} - -export const checkFileExists = async (filePath: string): Promise => { - try { - await fs.access(filePath, fs.constants.F_OK) - return true - } catch (err) { - log(err) - return false - } -} - -export const getEntities = async (filePath: string): Promise => { - const entitiesData = await fs.readFile(filePath, 'utf-8') - const entities = JSON.parse(entitiesData) - return entities -} - -export const loadAndSaveData = async ( - entityType: EntityTarget, - dataSource: DataSource, - entities: any, - relations?: any | undefined -): Promise => { - const entityRepository = dataSource.getRepository(entityType) - - const savedEntity: Entity[] = [] - - for (const entityData of entities) { - let entity = entityRepository.create(entityData as DeepPartial) - - if (relations) { - for (const field in relations) { - const valueIndex = `${field}Index` - - entity = { - ...entity, - [field]: relations[field][entityData[valueIndex]] - } - } - } - const dbEntity = await entityRepository.save(entity) - savedEntity.push(dbEntity) - } - - return savedEntity -} - -export const sleep = async (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)) - -export const getRepoDetails = async ( - octokit: Octokit, - repository: string, - commitHash: string | undefined -): Promise<{ - repo: string - packageJSON: PackageJSON - repoUrl: string -}> => { - const [owner, repo] = repository.split('/') - const { data: packageJSONData } = await octokit.rest.repos.getContent({ - owner, - repo, - path: 'package.json', - ref: commitHash - }) - - if (!packageJSONData) { - throw new Error('Package.json file not found') - } - - assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file') - const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content)) - - assert(packageJSON.name, "name field doesn't exist in package.json") - - const repoUrl = ( - await octokit.rest.repos.get({ - owner, - repo - }) - ).data.html_url - - return { - repo, - packageJSON, - repoUrl - } -} - -// Wrapper method for registry txs to retry once if 'account sequence mismatch' occurs -export const registryTransactionWithRetry = async ( - txMethod: () => Promise -): Promise => { - try { - return await txMethod() - } catch (error: any) { - if (!error.message.includes('account sequence mismatch')) { - throw error - } - - console.error( - 'Transaction failed due to account sequence mismatch. Retrying...' - ) - - try { - return await txMethod() - } catch (retryError: any) { - throw new Error( - `Transaction failed again after retry: ${retryError.message}` - ) - } - } -} diff --git a/apps/backend/test/delete-db.ts b/apps/backend/test/delete-db.ts deleted file mode 100644 index 1872b30..0000000 --- a/apps/backend/test/delete-db.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as fs from 'node:fs/promises' -import debug from 'debug' - -import { getConfig } from '../src/utils' - -const log = debug('snowball:delete-database') - -const deleteFile = async (filePath: string) => { - await fs.unlink(filePath) - log(`File ${filePath} has been deleted.`) -} - -const main = async () => { - const config = await getConfig() - - deleteFile(config.database.dbPath) -} - -main().catch((err) => log(err)) diff --git a/apps/backend/test/fixtures/deployments.json b/apps/backend/test/fixtures/deployments.json deleted file mode 100644 index b73956f..0000000 --- a/apps/backend/test/fixtures/deployments.json +++ /dev/null @@ -1,189 +0,0 @@ -[ - { - "projectIndex": 0, - "domainIndex": 0, - "createdByIndex": 0, - "id": "ffhae3zq", - "status": "Ready", - "environment": "Production", - "isCurrent": true, - "applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationRecordData": {}, - "applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, - "branch": "main", - "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", - "commitMessage": "subscription added", - "url": "testProject-ffhae3zq.snowball.xyz" - }, - { - "projectIndex": 0, - "domainIndex": 1, - "createdByIndex": 0, - "id": "vehagei8", - "status": "Ready", - "environment": "Preview", - "isCurrent": false, - "applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi", - "applicationRecordData": {}, - "applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, - "branch": "test", - "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", - "commitMessage": "subscription added", - "url": "testProject-vehagei8.snowball.xyz" - }, - { - "projectIndex": 0, - "domainIndex": 2, - "createdByIndex": 0, - "id": "qmgekyte", - "status": "Ready", - "environment": "Development", - "isCurrent": false, - "applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", - "applicationRecordData": {}, - "applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, - "branch": "test", - "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", - "commitMessage": "subscription added", - "url": "testProject-qmgekyte.snowball.xyz" - }, - { - "projectIndex": 0, - "domainIndex": null, - "createdByIndex": 0, - "id": "f8wsyim6", - "status": "Ready", - "environment": "Production", - "isCurrent": false, - "applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi", - "applicationRecordData": {}, - "applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, - "branch": "prod", - "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", - "commitMessage": "subscription added", - "url": "testProject-f8wsyim6.snowball.xyz" - }, - { - "projectIndex": 1, - "domainIndex": 3, - "createdByIndex": 1, - "id": "eO8cckxk", - "status": "Ready", - "environment": "Production", - "isCurrent": true, - "applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationRecordData": {}, - "applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, - "branch": "main", - "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", - "commitMessage": "subscription added", - "url": "testProject-2-eO8cckxk.snowball.xyz" - }, - { - "projectIndex": 1, - "domainIndex": 4, - "createdByIndex": 1, - "id": "yaq0t5yw", - "status": "Ready", - "environment": "Preview", - "isCurrent": false, - "applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", - "applicationRecordData": {}, - "applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, - "branch": "test", - "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", - "commitMessage": "subscription added", - "url": "testProject-2-yaq0t5yw.snowball.xyz" - }, - { - "projectIndex": 1, - "domainIndex": 5, - "createdByIndex": 1, - "id": "hwwr6sbx", - "status": "Ready", - "environment": "Development", - "isCurrent": false, - "applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi", - "applicationRecordData": {}, - "applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, - "branch": "test", - "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", - "commitMessage": "subscription added", - "url": "testProject-2-hwwr6sbx.snowball.xyz" - }, - { - "projectIndex": 2, - "domainIndex": 9, - "createdByIndex": 2, - "id": "ndxje48a", - "status": "Ready", - "environment": "Production", - "isCurrent": true, - "applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationRecordData": {}, - "applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, - "branch": "main", - "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", - "commitMessage": "subscription added", - "url": "iglootools-ndxje48a.snowball.xyz" - }, - { - "projectIndex": 2, - "domainIndex": 7, - "createdByIndex": 2, - "id": "gtgpgvei", - "status": "Ready", - "environment": "Preview", - "isCurrent": false, - "applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationRecordData": {}, - "applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, - "branch": "test", - "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", - "commitMessage": "subscription added", - "url": "iglootools-gtgpgvei.snowball.xyz" - }, - { - "projectIndex": 2, - "domainIndex": 8, - "createdByIndex": 2, - "id": "b4bpthjr", - "status": "Ready", - "environment": "Development", - "isCurrent": false, - "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", - "applicationRecordData": {}, - "applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, - "branch": "test", - "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", - "commitMessage": "subscription added", - "url": "iglootools-b4bpthjr.snowball.xyz" - }, - { - "projectIndex": 3, - "domainIndex": 6, - "createdByIndex": 2, - "id": "b4bpthjr", - "status": "Ready", - "environment": "Production", - "isCurrent": true, - "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", - "applicationRecordData": {}, - "applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, - "branch": "test", - "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", - "commitMessage": "subscription added", - "url": "iglootools-b4bpthjr.snowball.xyz" - } -] diff --git a/apps/backend/test/fixtures/environment-variables.json b/apps/backend/test/fixtures/environment-variables.json deleted file mode 100644 index 2af52b3..0000000 --- a/apps/backend/test/fixtures/environment-variables.json +++ /dev/null @@ -1,92 +0,0 @@ -[ - { - "projectIndex": 0, - "key": "ABC", - "value": "ABC", - "environment": "Production" - }, - { - "projectIndex": 0, - "key": "ABC", - "value": "ABC", - "environment": "Preview" - }, - { - "projectIndex": 0, - "key": "XYZ", - "value": "abc3", - "environment": "Preview" - }, - { - "projectIndex": 1, - "key": "ABC", - "value": "ABC", - "environment": "Production" - }, - { - "projectIndex": 1, - "key": "ABC", - "value": "ABC", - "environment": "Preview" - }, - { - "projectIndex": 1, - "key": "XYZ", - "value": "abc3", - "environment": "Preview" - }, - { - "projectIndex": 2, - "key": "ABC", - "value": "ABC", - "environment": "Production" - }, - { - "projectIndex": 2, - "key": "ABC", - "value": "ABC", - "environment": "Preview" - }, - { - "projectIndex": 2, - "key": "XYZ", - "value": "abc3", - "environment": "Preview" - }, - { - "projectIndex": 3, - "key": "ABC", - "value": "ABC", - "environment": "Production" - }, - { - "projectIndex": 3, - "key": "ABC", - "value": "ABC", - "environment": "Preview" - }, - { - "projectIndex": 3, - "key": "XYZ", - "value": "abc3", - "environment": "Preview" - }, - { - "projectIndex": 4, - "key": "ABC", - "value": "ABC", - "environment": "Production" - }, - { - "projectIndex": 4, - "key": "ABC", - "value": "ABC", - "environment": "Preview" - }, - { - "projectIndex": 4, - "key": "XYZ", - "value": "abc3", - "environment": "Preview" - } -] diff --git a/apps/backend/test/fixtures/organizations.json b/apps/backend/test/fixtures/organizations.json deleted file mode 100644 index 0d688fb..0000000 --- a/apps/backend/test/fixtures/organizations.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "id": "2379cf1f-a232-4ad2-ae14-4d881131cc26", - "name": "Deploy Tools", - "slug": "deploy-tools" - } -] diff --git a/apps/backend/test/fixtures/primary-domains.json b/apps/backend/test/fixtures/primary-domains.json deleted file mode 100644 index 23e2666..0000000 --- a/apps/backend/test/fixtures/primary-domains.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - { - "projectIndex": 0, - "name": "example.snowballtools.xyz", - "status": "Live", - "branch": "main" - }, - { - "projectIndex": 0, - "name": "example.org", - "status": "Pending", - "branch": "test" - }, - { - "projectIndex": 1, - "name": "example.snowballtools.xyz", - "status": "Live", - "branch": "main" - }, - { - "projectIndex": 1, - "name": "example.org", - "status": "Pending", - "branch": "test" - }, - { - "projectIndex": 2, - "name": "example.snowballtools.xyz", - "status": "Live", - "branch": "main" - }, - { - "projectIndex": 2, - "name": "example.org", - "status": "Pending", - "branch": "test" - }, - { - "projectIndex": 3, - "name": "iglootools-2.com", - "status": "Pending", - "branch": "test" - } -] diff --git a/apps/backend/test/fixtures/project-members.json b/apps/backend/test/fixtures/project-members.json deleted file mode 100644 index 84029c2..0000000 --- a/apps/backend/test/fixtures/project-members.json +++ /dev/null @@ -1,56 +0,0 @@ -[ - { - "memberIndex": 1, - "projectIndex": 0, - "permissions": ["View"], - "isPending": false - }, - { - "memberIndex": 2, - "projectIndex": 0, - "permissions": ["View", "Edit"], - "isPending": false - }, - { - "memberIndex": 2, - "projectIndex": 1, - "permissions": ["View"], - "isPending": false - }, - { - "memberIndex": 0, - "projectIndex": 2, - "permissions": ["View"], - "isPending": false - }, - { - "memberIndex": 1, - "projectIndex": 2, - "permissions": ["View", "Edit"], - "isPending": false - }, - { - "memberIndex": 0, - "projectIndex": 3, - "permissions": ["View"], - "isPending": false - }, - { - "memberIndex": 2, - "projectIndex": 3, - "permissions": ["View", "Edit"], - "isPending": false - }, - { - "memberIndex": 1, - "projectIndex": 4, - "permissions": ["View"], - "isPending": false - }, - { - "memberIndex": 2, - "projectIndex": 4, - "permissions": ["View", "Edit"], - "isPending": false - } -] diff --git a/apps/backend/test/fixtures/projects.json b/apps/backend/test/fixtures/projects.json deleted file mode 100644 index f456464..0000000 --- a/apps/backend/test/fixtures/projects.json +++ /dev/null @@ -1,67 +0,0 @@ -[ - { - "ownerIndex": 0, - "organizationIndex": 0, - "name": "testProject", - "repository": "snowball-tools/snowball-ts-framework-template", - "prodBranch": "main", - "description": "test", - "template": "webapp", - "framework": "test", - "webhooks": [], - "icon": "", - "subDomain": "testProject.snowball.xyz" - }, - { - "ownerIndex": 1, - "organizationIndex": 0, - "name": "testProject-2", - "repository": "snowball-tools/snowball-ts-framework-template", - "prodBranch": "main", - "description": "test-2", - "template": "webapp", - "framework": "test-2", - "webhooks": [], - "icon": "", - "subDomain": "testProject-2.snowball.xyz" - }, - { - "ownerIndex": 2, - "organizationIndex": 0, - "name": "iglootools", - "repository": "snowball-tools/snowball-ts-framework-template", - "prodBranch": "main", - "description": "test-3", - "template": "webapp", - "framework": "test-3", - "webhooks": [], - "icon": "", - "subDomain": "iglootools.snowball.xyz" - }, - { - "ownerIndex": 1, - "organizationIndex": 0, - "name": "iglootools-2", - "repository": "snowball-tools/snowball-ts-framework-template", - "prodBranch": "main", - "description": "test-4", - "template": "webapp", - "framework": "test-4", - "webhooks": [], - "icon": "", - "subDomain": "iglootools-2.snowball.xyz" - }, - { - "ownerIndex": 0, - "organizationIndex": 1, - "name": "snowball-2", - "repository": "snowball-tools/snowball-ts-framework-template", - "prodBranch": "main", - "description": "test-5", - "template": "webapp", - "framework": "test-5", - "webhooks": [], - "icon": "", - "subDomain": "snowball-2.snowball.xyz" - } -] diff --git a/apps/backend/test/fixtures/redirected-domains.json b/apps/backend/test/fixtures/redirected-domains.json deleted file mode 100644 index 64fa973..0000000 --- a/apps/backend/test/fixtures/redirected-domains.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "projectIndex": 0, - "name": "www.example.org", - "status": "Pending", - "redirectToIndex": 1, - "branch": "test" - }, - { - "projectIndex": 1, - "name": "www.example.org", - "status": "Pending", - "redirectToIndex": 3, - "branch": "test" - }, - { - "projectIndex": 2, - "name": "www.example.org", - "status": "Pending", - "redirectToIndex": 5, - "branch": "test" - } -] diff --git a/apps/backend/test/fixtures/user-organizations.json b/apps/backend/test/fixtures/user-organizations.json deleted file mode 100644 index adff4ae..0000000 --- a/apps/backend/test/fixtures/user-organizations.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "role": "Owner", - "memberIndex": 0, - "organizationIndex": 0 - }, - { - "role": "Maintainer", - "memberIndex": 1, - "organizationIndex": 0 - }, - { - "role": "Owner", - "memberIndex": 2, - "organizationIndex": 0 - }, - { - "role": "Owner", - "memberIndex": 0, - "organizationIndex": 1 - } -] diff --git a/apps/backend/test/fixtures/users.json b/apps/backend/test/fixtures/users.json deleted file mode 100644 index bc1a44d..0000000 --- a/apps/backend/test/fixtures/users.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "id": "59f4355d-9549-4aac-9b54-eeefceeabef0", - "name": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "email": "snowball@snowballtools.xyz", - "isVerified": true, - "ethAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - { - "id": "e505b212-8da6-48b2-9614-098225dab34b", - "name": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8", - "email": "alice@snowballtools.xyz", - "isVerified": true, - "ethAddress": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8" - }, - { - "id": "cd892fad-9138-4aa2-a62c-414a32776ea7", - "name": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a", - "email": "bob@snowballtools.xyz", - "isVerified": true, - "ethAddress": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a" - } -] diff --git a/apps/backend/test/initialize-db.ts b/apps/backend/test/initialize-db.ts deleted file mode 100644 index 3aff317..0000000 --- a/apps/backend/test/initialize-db.ts +++ /dev/null @@ -1,176 +0,0 @@ -import path from 'node:path' -import debug from 'debug' -import { DataSource } from 'typeorm' - -import { Deployment } from '../src/entity/Deployment' -import { Domain } from '../src/entity/Domain' -import { EnvironmentVariable } from '../src/entity/EnvironmentVariable' -import { Organization } from '../src/entity/Organization' -import { Project } from '../src/entity/Project' -import { ProjectMember } from '../src/entity/ProjectMember' -import { User } from '../src/entity/User' -import { UserOrganization } from '../src/entity/UserOrganization' -import { - checkFileExists, - getConfig, - getEntities, - loadAndSaveData -} from '../src/utils' - -const log = debug('snowball:initialize-database') - -const USER_DATA_PATH = './fixtures/users.json' -const PROJECT_DATA_PATH = './fixtures/projects.json' -const ORGANIZATION_DATA_PATH = './fixtures/organizations.json' -const USER_ORGANIZATION_DATA_PATH = './fixtures/user-organizations.json' -const PROJECT_MEMBER_DATA_PATH = './fixtures/project-members.json' -const PRIMARY_DOMAIN_DATA_PATH = './fixtures/primary-domains.json' -const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json' -const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json' -const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json' - -const generateTestData = async (dataSource: DataSource) => { - const userEntities = await getEntities( - path.resolve(__dirname, USER_DATA_PATH) - ) - const savedUsers = await loadAndSaveData(User, dataSource, userEntities) - - const orgEntities = await getEntities( - path.resolve(__dirname, ORGANIZATION_DATA_PATH) - ) - const savedOrgs = await loadAndSaveData(Organization, dataSource, orgEntities) - - const projectRelations = { - owner: savedUsers, - organization: savedOrgs - } - - const projectEntities = await getEntities( - path.resolve(__dirname, PROJECT_DATA_PATH) - ) - const savedProjects = await loadAndSaveData( - Project, - dataSource, - projectEntities, - projectRelations - ) - - const domainRepository = dataSource.getRepository(Domain) - - const domainPrimaryRelations = { - project: savedProjects - } - - const primaryDomainsEntities = await getEntities( - path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH) - ) - const savedPrimaryDomains = await loadAndSaveData( - Domain, - dataSource, - primaryDomainsEntities, - domainPrimaryRelations - ) - - const domainRedirectedRelations = { - project: savedProjects, - redirectTo: savedPrimaryDomains - } - - const redirectDomainsEntities = await getEntities( - path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH) - ) - await loadAndSaveData( - Domain, - dataSource, - redirectDomainsEntities, - domainRedirectedRelations - ) - - const savedDomains = await domainRepository.find() - - const userOrganizationRelations = { - member: savedUsers, - organization: savedOrgs - } - - const userOrganizationsEntities = await getEntities( - path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH) - ) - await loadAndSaveData( - UserOrganization, - dataSource, - userOrganizationsEntities, - userOrganizationRelations - ) - - const projectMemberRelations = { - member: savedUsers, - project: savedProjects - } - - const projectMembersEntities = await getEntities( - path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH) - ) - await loadAndSaveData( - ProjectMember, - dataSource, - projectMembersEntities, - projectMemberRelations - ) - - const deploymentRelations = { - project: savedProjects, - domain: savedDomains, - createdBy: savedUsers - } - - const deploymentsEntities = await getEntities( - path.resolve(__dirname, DEPLOYMENT_DATA_PATH) - ) - await loadAndSaveData( - Deployment, - dataSource, - deploymentsEntities, - deploymentRelations - ) - - const environmentVariableRelations = { - project: savedProjects - } - - const environmentVariablesEntities = await getEntities( - path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH) - ) - await loadAndSaveData( - EnvironmentVariable, - dataSource, - environmentVariablesEntities, - environmentVariableRelations - ) -} - -const main = async () => { - const config = await getConfig() - const isDbPresent = await checkFileExists(config.database.dbPath) - - if (!isDbPresent) { - const dataSource = new DataSource({ - type: 'better-sqlite3', - database: config.database.dbPath, - synchronize: true, - logging: true, - entities: [path.join(__dirname, '../src/entity/*')] - }) - - await dataSource.initialize() - - await generateTestData(dataSource) - log('Data loaded successfully') - } else { - log('WARNING: Database already exists') - } -} - -main().catch((err) => { - log(err) -}) diff --git a/apps/backend/test/initialize-registry.ts b/apps/backend/test/initialize-registry.ts deleted file mode 100644 index 27c303d..0000000 --- a/apps/backend/test/initialize-registry.ts +++ /dev/null @@ -1,49 +0,0 @@ -import debug from 'debug' - -import { Registry, parseGasAndFees } from '@cerc-io/registry-sdk' - -import { getConfig } from '../src/utils' - -const log = debug('snowball:initialize-registry') - -const DENOM = 'alnt' -const BOND_AMOUNT = '1000000000' - -async function main() { - const { registryConfig } = await getConfig() - - // TODO: Get authority names from args - const authorityNames = ['snowballtools', registryConfig.authority] - - const registry = new Registry( - registryConfig.gqlEndpoint, - registryConfig.restEndpoint, - { chainId: registryConfig.chainId } - ) - - const bondId = await registry.getNextBondId(registryConfig.privateKey) - log('bondId:', bondId) - - const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees) - - await registry.createBond( - { denom: DENOM, amount: BOND_AMOUNT }, - registryConfig.privateKey, - fee - ) - - for await (const name of authorityNames) { - await registry.reserveAuthority({ name }, registryConfig.privateKey, fee) - log('Reserved authority name:', name) - await registry.setAuthorityBond( - { name, bondId }, - registryConfig.privateKey, - fee - ) - log(`Bond ${bondId} set for authority ${name}`) - } -} - -main().catch((err) => { - log(err) -}) diff --git a/apps/backend/test/publish-deploy-records.ts b/apps/backend/test/publish-deploy-records.ts deleted file mode 100644 index 61d424e..0000000 --- a/apps/backend/test/publish-deploy-records.ts +++ /dev/null @@ -1,100 +0,0 @@ -import path from 'node:path' -import debug from 'debug' -import { DataSource } from 'typeorm' - -import { Registry, parseGasAndFees } from '@cerc-io/registry-sdk' - -import { - Deployment, - DeploymentStatus, - Environment -} from '../src/entity/Deployment' -import { getConfig } from '../src/utils' - -const log = debug('snowball:publish-deploy-records') - -async function main() { - const { registryConfig, database } = await getConfig() - - const registry = new Registry( - registryConfig.gqlEndpoint, - registryConfig.restEndpoint, - { chainId: registryConfig.chainId } - ) - - const dataSource = new DataSource({ - type: 'better-sqlite3', - database: database.dbPath, - synchronize: true, - entities: [path.join(__dirname, '../src/entity/*')] - }) - - await dataSource.initialize() - - const deploymentRepository = dataSource.getRepository(Deployment) - const deployments = await deploymentRepository.find({ - relations: { - project: true - }, - where: { - status: DeploymentStatus.Building - } - }) - - for await (const deployment of deployments) { - const url = `https://${(deployment.project.name).toLowerCase()}-${deployment.id}.${deployment.deployer.baseDomain}` - - const applicationDeploymentRecord = { - type: 'ApplicationDeploymentRecord', - version: '0.0.1', - name: deployment.applicationRecordData.name, - application: deployment.applicationRecordId, - - // TODO: Create DNS record - dns: 'bafyreihlymqggsgqiqawvehkpr2imt4l3u6q7um7xzjrux5rhsvwnuyewm', - - // Using dummy values - meta: JSON.stringify({ - config: 'da39a3ee5e6b4b0d3255bfef95601890afd80709', - so: '66fcfa49a1664d4cb4ce4f72c1c0e151' - }), - - request: deployment.applicationDeploymentRequestId, - url - } - - const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees) - - const result = await registry.setRecord( - { - privateKey: registryConfig.privateKey, - record: applicationDeploymentRecord, - bondId: registryConfig.bondId - }, - '', - fee - ) - - // Remove deployment for project subdomain if deployment is for production environment - if (deployment.environment === Environment.Production) { - applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.deployer.baseDomain}` - - await registry.setRecord( - { - privateKey: registryConfig.privateKey, - record: applicationDeploymentRecord, - bondId: registryConfig.bondId - }, - '', - fee - ) - } - - log('Application deployment record data:', applicationDeploymentRecord) - log(`Application deployment record published: ${result.id}`) - } -} - -main().catch((err) => { - log(err) -}) diff --git a/apps/backend/test/publish-deployment-removal-records.ts b/apps/backend/test/publish-deployment-removal-records.ts deleted file mode 100644 index e902fc7..0000000 --- a/apps/backend/test/publish-deployment-removal-records.ts +++ /dev/null @@ -1,70 +0,0 @@ -import path from 'node:path' -import debug from 'debug' -import { DataSource } from 'typeorm' - -import { Registry, parseGasAndFees } from '@cerc-io/registry-sdk' - -import { Deployment, DeploymentStatus } from '../src/entity/Deployment' -import { getConfig } from '../src/utils' - -const log = debug('snowball:publish-deployment-removal-records') - -async function main() { - const { registryConfig, database } = await getConfig() - - const registry = new Registry( - registryConfig.gqlEndpoint, - registryConfig.restEndpoint, - { chainId: registryConfig.chainId } - ) - - const dataSource = new DataSource({ - type: 'better-sqlite3', - database: database.dbPath, - synchronize: true, - entities: [path.join(__dirname, '../src/entity/*')] - }) - - await dataSource.initialize() - - const deploymentRepository = dataSource.getRepository(Deployment) - const deployments = await deploymentRepository.find({ - relations: { - project: true - }, - where: { - status: DeploymentStatus.Deleting - } - }) - - for await (const deployment of deployments) { - const applicationDeploymentRemovalRecord = { - type: 'ApplicationDeploymentRemovalRecord', - version: '1.0.0', - deployment: deployment.applicationDeploymentRecordId, - request: deployment.applicationDeploymentRemovalRequestId - } - - const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees) - - const result = await registry.setRecord( - { - privateKey: registryConfig.privateKey, - record: applicationDeploymentRemovalRecord, - bondId: registryConfig.bondId - }, - '', - fee - ) - - log( - 'Application deployment removal record data:', - applicationDeploymentRemovalRecord - ) - log(`Application deployment removal record published: ${result.id}`) - } -} - -main().catch((err) => { - log(err) -}) diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json deleted file mode 100644 index d4cfe92..0000000 --- a/apps/backend/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "baseUrl": ".", - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"], - "exclude": ["dist", "src/**/*.test.ts"] -}