Compare commits
No commits in common. "ng-deploy-with-fixes" and "main" have entirely different histories.
ng-deploy-
...
main
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"enableAllProjectMcpServers": false
|
|
||||||
}
|
|
||||||
76
apps/backend/README.md
Normal file
76
apps/backend/README.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# 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)
|
||||||
32
apps/backend/biome.json
Normal file
32
apps/backend/biome.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"$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/**/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
43
apps/backend/environments/local.toml
Normal file
43
apps/backend/environments/local.toml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
[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"
|
||||||
43
apps/backend/environments/local.toml.example
Normal file
43
apps/backend/environments/local.toml.example
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
[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"
|
||||||
68
apps/backend/package.json
Normal file
68
apps/backend/package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
66
apps/backend/src/config.ts
Normal file
66
apps/backend/src/config.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
7
apps/backend/src/constants.ts
Normal file
7
apps/backend/src/constants.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
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'
|
||||||
694
apps/backend/src/database.ts
Normal file
694
apps/backend/src/database.ts
Normal file
@ -0,0 +1,694 @@
|
|||||||
|
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<void> {
|
||||||
|
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<User>): Promise<User | null> {
|
||||||
|
const userRepository = this.dataSource.getRepository(User)
|
||||||
|
const user = await userRepository.findOne(options)
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
async addUser(data: DeepPartial<User>): Promise<User> {
|
||||||
|
const userRepository = this.dataSource.getRepository(User)
|
||||||
|
const user = await userRepository.save(data)
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUser(user: User, data: DeepPartial<User>): Promise<boolean> {
|
||||||
|
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<Organization>
|
||||||
|
): Promise<Organization[]> {
|
||||||
|
const organizationRepository = this.dataSource.getRepository(Organization)
|
||||||
|
const organizations = await organizationRepository.find(options)
|
||||||
|
|
||||||
|
return organizations
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganization(
|
||||||
|
options: FindOneOptions<Organization>
|
||||||
|
): Promise<Organization | null> {
|
||||||
|
const organizationRepository = this.dataSource.getRepository(Organization)
|
||||||
|
const organization = await organizationRepository.findOne(options)
|
||||||
|
|
||||||
|
return organization
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationsByUserId(userId: string): Promise<Organization[]> {
|
||||||
|
const organizationRepository = this.dataSource.getRepository(Organization)
|
||||||
|
|
||||||
|
const userOrgs = await organizationRepository.find({
|
||||||
|
where: {
|
||||||
|
userOrganizations: {
|
||||||
|
member: {
|
||||||
|
id: userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return userOrgs
|
||||||
|
}
|
||||||
|
|
||||||
|
async addUserOrganization(
|
||||||
|
data: DeepPartial<UserOrganization>
|
||||||
|
): Promise<UserOrganization> {
|
||||||
|
const userOrganizationRepository =
|
||||||
|
this.dataSource.getRepository(UserOrganization)
|
||||||
|
const newUserOrganization = await userOrganizationRepository.save(data)
|
||||||
|
|
||||||
|
return newUserOrganization
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOrganization(
|
||||||
|
organizationId: string,
|
||||||
|
data: DeepPartial<Organization>
|
||||||
|
): Promise<boolean> {
|
||||||
|
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<Project>): Promise<Project[]> {
|
||||||
|
const projectRepository = this.dataSource.getRepository(Project)
|
||||||
|
const projects = await projectRepository.find(options)
|
||||||
|
|
||||||
|
return projects
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectById(projectId: string): Promise<Project | null> {
|
||||||
|
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<Project[]> {
|
||||||
|
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<Project[]> {
|
||||||
|
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<Deployment>
|
||||||
|
): Promise<Deployment[]> {
|
||||||
|
const deploymentRepository = this.dataSource.getRepository(Deployment)
|
||||||
|
const deployments = await deploymentRepository.find(options)
|
||||||
|
|
||||||
|
return deployments
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeploymentsByProjectId(projectId: string): Promise<Deployment[]> {
|
||||||
|
return this.getDeployments({
|
||||||
|
relations: {
|
||||||
|
project: true,
|
||||||
|
createdBy: true,
|
||||||
|
deployer: true
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
project: {
|
||||||
|
id: projectId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: 'DESC'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNonCanonicalDeploymentsByProjectId(
|
||||||
|
projectId: string
|
||||||
|
): Promise<Deployment[]> {
|
||||||
|
return this.getDeployments({
|
||||||
|
relations: {
|
||||||
|
project: true,
|
||||||
|
createdBy: true,
|
||||||
|
deployer: true
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
project: {
|
||||||
|
id: projectId
|
||||||
|
},
|
||||||
|
isCanonical: false
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: 'DESC'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeployment(
|
||||||
|
options: FindOneOptions<Deployment>
|
||||||
|
): Promise<Deployment | null> {
|
||||||
|
const deploymentRepository = this.dataSource.getRepository(Deployment)
|
||||||
|
const deployment = await deploymentRepository.findOne(options)
|
||||||
|
|
||||||
|
return deployment
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDomains(options: FindManyOptions<Domain>): Promise<Domain[]> {
|
||||||
|
const domainRepository = this.dataSource.getRepository(Domain)
|
||||||
|
const domains = await domainRepository.find(options)
|
||||||
|
|
||||||
|
return domains
|
||||||
|
}
|
||||||
|
|
||||||
|
async addDeployment(data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||||
|
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<ProjectMember[]> {
|
||||||
|
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<EnvironmentVariable>
|
||||||
|
): Promise<EnvironmentVariable[]> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable)
|
||||||
|
|
||||||
|
const environmentVariables = await environmentVariableRepository.find({
|
||||||
|
where: {
|
||||||
|
project: {
|
||||||
|
id: projectId
|
||||||
|
},
|
||||||
|
...filter
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return environmentVariables
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeProjectMemberById(projectMemberId: string): Promise<boolean> {
|
||||||
|
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<ProjectMember>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const projectMemberRepository = this.dataSource.getRepository(ProjectMember)
|
||||||
|
const updateResult = await projectMemberRepository.update(
|
||||||
|
{ id: projectMemberId },
|
||||||
|
data
|
||||||
|
)
|
||||||
|
|
||||||
|
return Boolean(updateResult.affected)
|
||||||
|
}
|
||||||
|
|
||||||
|
async addProjectMember(
|
||||||
|
data: DeepPartial<ProjectMember>
|
||||||
|
): Promise<ProjectMember> {
|
||||||
|
const projectMemberRepository = this.dataSource.getRepository(ProjectMember)
|
||||||
|
const newProjectMember = await projectMemberRepository.save(data)
|
||||||
|
|
||||||
|
return newProjectMember
|
||||||
|
}
|
||||||
|
|
||||||
|
async addEnvironmentVariables(
|
||||||
|
data: DeepPartial<EnvironmentVariable>[]
|
||||||
|
): Promise<EnvironmentVariable[]> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable)
|
||||||
|
const savedEnvironmentVariables =
|
||||||
|
await environmentVariableRepository.save(data)
|
||||||
|
|
||||||
|
return savedEnvironmentVariables
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEnvironmentVariable(
|
||||||
|
environmentVariableId: string,
|
||||||
|
data: DeepPartial<EnvironmentVariable>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable)
|
||||||
|
const updateResult = await environmentVariableRepository.update(
|
||||||
|
{ id: environmentVariableId },
|
||||||
|
data
|
||||||
|
)
|
||||||
|
|
||||||
|
return Boolean(updateResult.affected)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEnvironmentVariable(
|
||||||
|
environmentVariableId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
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<ProjectMember> {
|
||||||
|
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<Project[]> {
|
||||||
|
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<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.updateDeployment({ id: deploymentId }, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDeployment(
|
||||||
|
criteria: FindOptionsWhere<Deployment>,
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const deploymentRepository = this.dataSource.getRepository(Deployment)
|
||||||
|
const updateResult = await deploymentRepository.update(criteria, data)
|
||||||
|
|
||||||
|
return Boolean(updateResult.affected)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDeploymentsByProjectIds(
|
||||||
|
projectIds: string[],
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<Project>
|
||||||
|
): Promise<Project> {
|
||||||
|
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<Project> {
|
||||||
|
const projectRepository = this.dataSource.getRepository(Project)
|
||||||
|
|
||||||
|
return projectRepository.save(project)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProjectById(
|
||||||
|
projectId: string,
|
||||||
|
data: DeepPartial<Project>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const projectRepository = this.dataSource.getRepository(Project)
|
||||||
|
const updateResult = await projectRepository.update({ id: projectId }, data)
|
||||||
|
|
||||||
|
return Boolean(updateResult.affected)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteProjectById(projectId: string): Promise<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<Domain>): Promise<Domain> {
|
||||||
|
const domainRepository = this.dataSource.getRepository(Domain)
|
||||||
|
const newDomain = await domainRepository.save(data)
|
||||||
|
|
||||||
|
return newDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDomain(options: FindOneOptions<Domain>): Promise<Domain | null> {
|
||||||
|
const domainRepository = this.dataSource.getRepository(Domain)
|
||||||
|
const domain = await domainRepository.findOne(options)
|
||||||
|
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDomainById(
|
||||||
|
domainId: string,
|
||||||
|
data: DeepPartial<Domain>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const domainRepository = this.dataSource.getRepository(Domain)
|
||||||
|
const updateResult = await domainRepository.update({ id: domainId }, data)
|
||||||
|
|
||||||
|
return Boolean(updateResult.affected)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDomainsByProjectId(
|
||||||
|
projectId: string,
|
||||||
|
filter?: FindOptionsWhere<Domain>
|
||||||
|
): Promise<Domain[]> {
|
||||||
|
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<Domain | null> {
|
||||||
|
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<DNSRecordAttributes | null> {
|
||||||
|
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<Deployer>): Promise<Deployer> {
|
||||||
|
const deployerRepository = this.dataSource.getRepository(Deployer)
|
||||||
|
const newDomain = await deployerRepository.save(data)
|
||||||
|
|
||||||
|
return newDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeployers(): Promise<Deployer[]> {
|
||||||
|
const deployerRepository = this.dataSource.getRepository(Deployer)
|
||||||
|
const deployers = await deployerRepository.find()
|
||||||
|
return deployers
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeployerByLRN(deployerLrn: string): Promise<Deployer | null> {
|
||||||
|
const deployerRepository = this.dataSource.getRepository(Deployer)
|
||||||
|
const deployer = await deployerRepository.findOne({
|
||||||
|
where: { deployerLrn }
|
||||||
|
})
|
||||||
|
|
||||||
|
return deployer
|
||||||
|
}
|
||||||
|
}
|
||||||
32
apps/backend/src/entity/Deployer.ts
Normal file
32
apps/backend/src/entity/Deployer.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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[]
|
||||||
|
}
|
||||||
159
apps/backend/src/entity/Deployment.ts
Normal file
159
apps/backend/src/entity/Deployment.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
59
apps/backend/src/entity/Domain.ts
Normal file
59
apps/backend/src/entity/Domain.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
44
apps/backend/src/entity/EnvironmentVariable.ts
Normal file
44
apps/backend/src/entity/EnvironmentVariable.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
38
apps/backend/src/entity/Organization.ts
Normal file
38
apps/backend/src/entity/Organization.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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[]
|
||||||
|
}
|
||||||
111
apps/backend/src/entity/Project.ts
Normal file
111
apps/backend/src/entity/Project.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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[]
|
||||||
|
}
|
||||||
57
apps/backend/src/entity/ProjectMember.ts
Normal file
57
apps/backend/src/entity/ProjectMember.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
65
apps/backend/src/entity/User.ts
Normal file
65
apps/backend/src/entity/User.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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[]
|
||||||
|
}
|
||||||
47
apps/backend/src/entity/UserOrganization.ts
Normal file
47
apps/backend/src/entity/UserOrganization.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
53
apps/backend/src/index.ts
Normal file
53
apps/backend/src/index.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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<void> => {
|
||||||
|
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)
|
||||||
|
})
|
||||||
624
apps/backend/src/registry.ts
Normal file
624
apps/backend/src/registry.ts
Normal file
@ -0,0 +1,624 @@
|
|||||||
|
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<Deployment>
|
||||||
|
): 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<DeployerRecord[]> {
|
||||||
|
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<any> {
|
||||||
|
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<AppDeploymentRecord[]> {
|
||||||
|
// 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<AppDeploymentRemovalRecord[]> {
|
||||||
|
// 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<RegistryRecord | null> {
|
||||||
|
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<string[]> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
return this.registry.resolveNames([name])
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuctionData(auctionId: string): Promise<any> {
|
||||||
|
return this.registry.getAuctionsByIds([auctionId])
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTokensToAccount(
|
||||||
|
receiverAddress: string,
|
||||||
|
amount: string
|
||||||
|
): Promise<DeliverTxResponse> {
|
||||||
|
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<Account> {
|
||||||
|
const account = new Account(
|
||||||
|
Buffer.from(this.registryConfig.privateKey, 'hex')
|
||||||
|
)
|
||||||
|
await account.init()
|
||||||
|
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTxResponse(txHash: string): Promise<IndexedTx | null> {
|
||||||
|
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<string> {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
413
apps/backend/src/resolvers.ts
Normal file
413
apps/backend/src/resolvers.ts
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
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<any> => {
|
||||||
|
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<Domain> }
|
||||||
|
) => {
|
||||||
|
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<EnvironmentVariable>
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
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<Project>
|
||||||
|
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<Project> }
|
||||||
|
) => {
|
||||||
|
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<Domain> }
|
||||||
|
) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
apps/backend/src/routes/auth.ts
Normal file
97
apps/backend/src/routes/auth.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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
|
||||||
26
apps/backend/src/routes/github.ts
Normal file
26
apps/backend/src/routes/github.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
9
apps/backend/src/routes/staging.ts
Normal file
9
apps/backend/src/routes/staging.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
|
||||||
|
const router: Router = Router()
|
||||||
|
|
||||||
|
router.get('/version', async (_req, res) => {
|
||||||
|
return res.send({ version: '0.0.9' })
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
337
apps/backend/src/schema.gql
Normal file
337
apps/backend/src/schema.gql
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
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!
|
||||||
|
}
|
||||||
130
apps/backend/src/server.ts
Normal file
130
apps/backend/src/server.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
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<ApolloServer> => {
|
||||||
|
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
|
||||||
|
}
|
||||||
1783
apps/backend/src/service.ts
Normal file
1783
apps/backend/src/service.ts
Normal file
File diff suppressed because it is too large
Load Diff
130
apps/backend/src/turnkey-backend.ts
Normal file
130
apps/backend/src/turnkey-backend.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
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<T>(
|
||||||
|
input: T | null | undefined,
|
||||||
|
errorMessage?: string
|
||||||
|
): T {
|
||||||
|
if (input == null) {
|
||||||
|
throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return input
|
||||||
|
}
|
||||||
124
apps/backend/src/types.ts
Normal file
124
apps/backend/src/types.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
160
apps/backend/src/utils.ts
Normal file
160
apps/backend/src/utils.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
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<Config>(
|
||||||
|
path.join(__dirname, '../environments/local.toml')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _getConfig = async <ConfigType>(
|
||||||
|
configFile: string
|
||||||
|
): Promise<ConfigType> => {
|
||||||
|
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<boolean> => {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath, fs.constants.F_OK)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
log(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEntities = async (filePath: string): Promise<any> => {
|
||||||
|
const entitiesData = await fs.readFile(filePath, 'utf-8')
|
||||||
|
const entities = JSON.parse(entitiesData)
|
||||||
|
return entities
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||||
|
entityType: EntityTarget<Entity>,
|
||||||
|
dataSource: DataSource,
|
||||||
|
entities: any,
|
||||||
|
relations?: any | undefined
|
||||||
|
): Promise<Entity[]> => {
|
||||||
|
const entityRepository = dataSource.getRepository(entityType)
|
||||||
|
|
||||||
|
const savedEntity: Entity[] = []
|
||||||
|
|
||||||
|
for (const entityData of entities) {
|
||||||
|
let entity = entityRepository.create(entityData as DeepPartial<Entity>)
|
||||||
|
|
||||||
|
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<void> =>
|
||||||
|
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<any>
|
||||||
|
): Promise<any> => {
|
||||||
|
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}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
apps/backend/test/delete-db.ts
Normal file
19
apps/backend/test/delete-db.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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))
|
||||||
189
apps/backend/test/fixtures/deployments.json
vendored
Normal file
189
apps/backend/test/fixtures/deployments.json
vendored
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
92
apps/backend/test/fixtures/environment-variables.json
vendored
Normal file
92
apps/backend/test/fixtures/environment-variables.json
vendored
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
7
apps/backend/test/fixtures/organizations.json
vendored
Normal file
7
apps/backend/test/fixtures/organizations.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
||||||
|
"name": "Deploy Tools",
|
||||||
|
"slug": "deploy-tools"
|
||||||
|
}
|
||||||
|
]
|
||||||
44
apps/backend/test/fixtures/primary-domains.json
vendored
Normal file
44
apps/backend/test/fixtures/primary-domains.json
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
56
apps/backend/test/fixtures/project-members.json
vendored
Normal file
56
apps/backend/test/fixtures/project-members.json
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
67
apps/backend/test/fixtures/projects.json
vendored
Normal file
67
apps/backend/test/fixtures/projects.json
vendored
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
23
apps/backend/test/fixtures/redirected-domains.json
vendored
Normal file
23
apps/backend/test/fixtures/redirected-domains.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
22
apps/backend/test/fixtures/user-organizations.json
vendored
Normal file
22
apps/backend/test/fixtures/user-organizations.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"role": "Owner",
|
||||||
|
"memberIndex": 0,
|
||||||
|
"organizationIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Maintainer",
|
||||||
|
"memberIndex": 1,
|
||||||
|
"organizationIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Owner",
|
||||||
|
"memberIndex": 2,
|
||||||
|
"organizationIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Owner",
|
||||||
|
"memberIndex": 0,
|
||||||
|
"organizationIndex": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
23
apps/backend/test/fixtures/users.json
vendored
Normal file
23
apps/backend/test/fixtures/users.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
176
apps/backend/test/initialize-db.ts
Normal file
176
apps/backend/test/initialize-db.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
49
apps/backend/test/initialize-registry.ts
Normal file
49
apps/backend/test/initialize-registry.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
100
apps/backend/test/publish-deploy-records.ts
Normal file
100
apps/backend/test/publish-deploy-records.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
70
apps/backend/test/publish-deployment-removal-records.ts
Normal file
70
apps/backend/test/publish-deployment-removal-records.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
13
apps/backend/tsconfig.json
Normal file
13
apps/backend/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["dist", "src/**/*.test.ts"]
|
||||||
|
}
|
||||||
@ -6,8 +6,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_OPTIONS='--inspect' next dev --turbopack",
|
"dev": "NODE_OPTIONS='--inspect' next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"build:compile": "next build --experimental-build-mode compile",
|
|
||||||
"build:generate": "next build --experimental-build-mode generate",
|
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"lint:fix": "biome check --write .",
|
"lint:fix": "biome check --write .",
|
||||||
|
|||||||
@ -63,11 +63,14 @@ export default async function Page() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: We're keeping the token approach for now, but aware it's not working
|
||||||
|
const authToken = githubAccount.accessToken;
|
||||||
|
|
||||||
// Try using GitHub token
|
// Try using GitHub token
|
||||||
let octokit;
|
let octokit;
|
||||||
try {
|
try {
|
||||||
octokit = new Octokit({
|
octokit = new Octokit({
|
||||||
auth: process.env.GITHUB_TOKEN
|
auth: authToken || process.env.GITHUB_TOKEN
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test with a simple request
|
// Test with a simple request
|
||||||
@ -142,7 +145,7 @@ export default async function Page() {
|
|||||||
Failed to access GitHub API
|
Failed to access GitHub API
|
||||||
</div>
|
</div>
|
||||||
<div className="text-red-500 mb-4">
|
<div className="text-red-500 mb-4">
|
||||||
{authError instanceof Error ? authError.message : 'An error occurred'}
|
{authError.message}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground text-center max-w-md mb-6">
|
<div className="text-muted-foreground text-center max-w-md mb-6">
|
||||||
<p>This issue may be related to how Clerk is managing the GitHub token.</p>
|
<p>This issue may be related to how Clerk is managing the GitHub token.</p>
|
||||||
@ -179,7 +182,7 @@ export default async function Page() {
|
|||||||
Failed to authenticate with GitHub
|
Failed to authenticate with GitHub
|
||||||
</div>
|
</div>
|
||||||
<div className="text-red-500">
|
<div className="text-red-500">
|
||||||
{error instanceof Error ? error.message : 'Unknown error occurred'}
|
{error.message}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { LaconicMark } from '@/components/assets/laconic-mark'
|
import { useEffect, useState } from 'react'
|
||||||
import { ConfigureStep } from '@/components/onboarding/configure-step/configure-step'
|
import { useRouter } from 'next/navigation'
|
||||||
import { ConnectStep } from '@/components/onboarding/connect-step/connect-step'
|
|
||||||
import { DeployStep } from '@/components/onboarding/deploy-step/deploy-step'
|
|
||||||
import { SuccessStep } from '@/components/onboarding/success-step/success-step'
|
|
||||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
|
||||||
import { X } from 'lucide-react'
|
import { X } from 'lucide-react'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||||
import { useEffect, useState } from 'react'
|
import { ConnectStep } from '@/components/onboarding/connect-step/connect-step'
|
||||||
|
import { ConfigureStep } from '@/components/onboarding/configure-step/configure-step'
|
||||||
|
import { DeployStep } from '@/components/onboarding/deploy-step/deploy-step'
|
||||||
|
import { SuccessStep } from '@/components/onboarding/success-step/success-step'
|
||||||
|
import { LaconicMark } from '@/components/assets/laconic-mark'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parent component for the onboarding flow
|
* Parent component for the onboarding flow
|
||||||
@ -55,21 +55,13 @@ export default function CreateProjectFlow() {
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
||||||
{/* Fixed dimensions modal container */}
|
{/* Fixed dimensions modal container */}
|
||||||
<div
|
<div className={`${isDarkMode ? 'bg-black' : 'bg-white'} rounded-xl overflow-hidden flex shadow-2xl w-[1000px] h-[620px]`}>
|
||||||
className={`${isDarkMode ? 'bg-black' : 'bg-white'} rounded-xl overflow-hidden flex shadow-2xl w-[1000px] h-[620px]`}
|
|
||||||
>
|
|
||||||
{/* Left sidebar with fixed width */}
|
{/* Left sidebar with fixed width */}
|
||||||
<div
|
<div className={`w-[280px] min-w-[280px] ${isDarkMode ? 'bg-zinc-900' : 'bg-zinc-50'} p-8 relative overflow-hidden border-r ${isDarkMode ? 'border-zinc-800' : 'border-zinc-200'}`}>
|
||||||
className={`w-[280px] min-w-[280px] ${isDarkMode ? 'bg-zinc-900' : 'bg-zinc-50'} p-8 relative overflow-hidden border-r ${isDarkMode ? 'border-zinc-800' : 'border-zinc-200'}`}
|
|
||||||
>
|
|
||||||
{/* Laconic logo */}
|
{/* Laconic logo */}
|
||||||
<div className="flex items-center gap-2 mb-12">
|
<div className="flex items-center gap-2 mb-12">
|
||||||
<LaconicMark className="h-8 w-8" />
|
<LaconicMark className="h-8 w-8" />
|
||||||
<span
|
<span className={`${isDarkMode ? 'text-white' : 'text-zinc-900'} text-xl font-bold`}>LACONIC</span>
|
||||||
className={`${isDarkMode ? 'text-white' : 'text-zinc-900'} text-xl font-bold`}
|
|
||||||
>
|
|
||||||
LACONIC
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Steps - clickable */}
|
{/* Steps - clickable */}
|
||||||
@ -80,71 +72,29 @@ export default function CreateProjectFlow() {
|
|||||||
onClick={() => navigateToStep('connect')}
|
onClick={() => navigateToStep('connect')}
|
||||||
>
|
>
|
||||||
<div className="mr-4">
|
<div className="mr-4">
|
||||||
<div
|
<div className={`w-10 h-10 rounded-lg ${currentStep === 'connect'
|
||||||
className={`w-10 h-10 rounded-lg ${
|
? (isDarkMode ? 'bg-white' : 'bg-black')
|
||||||
currentStep === 'connect'
|
: (isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200')
|
||||||
? isDarkMode
|
} flex items-center justify-center`}>
|
||||||
? 'bg-white'
|
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className={currentStep === 'connect'
|
||||||
: 'bg-black'
|
? (isDarkMode ? 'text-black' : 'text-white')
|
||||||
: isDarkMode
|
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
|
||||||
? 'bg-zinc-800'
|
}>
|
||||||
: 'bg-zinc-200'
|
|
||||||
} flex items-center justify-center`}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
fill="none"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className={
|
|
||||||
currentStep === 'connect'
|
|
||||||
? isDarkMode
|
|
||||||
? 'text-black'
|
|
||||||
: 'text-white'
|
|
||||||
: isDarkMode
|
|
||||||
? 'text-zinc-400'
|
|
||||||
: 'text-zinc-600'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
|
<path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
|
||||||
<line x1="8" y1="12" x2="16" y2="12"></line>
|
<line x1="8" y1="12" x2="16" y2="12"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className={`w-px h-10 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'} mx-auto mt-2`}></div>
|
||||||
className={`w-px h-10 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'} mx-auto mt-2`}
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3
|
<h3 className={`font-medium text-base ${currentStep === 'connect'
|
||||||
className={`font-medium text-base ${
|
? (isDarkMode ? 'text-white' : 'text-zinc-900')
|
||||||
currentStep === 'connect'
|
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
|
||||||
? isDarkMode
|
}`}>Connect</h3>
|
||||||
? 'text-white'
|
<p className={`text-sm ${currentStep === 'connect'
|
||||||
: 'text-zinc-900'
|
? (isDarkMode ? 'text-zinc-300' : 'text-zinc-700')
|
||||||
: isDarkMode
|
: (isDarkMode ? 'text-zinc-500' : 'text-zinc-500')
|
||||||
? 'text-zinc-400'
|
}`}>Connect and import a GitHub repo</p>
|
||||||
: 'text-zinc-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Connect
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
className={`text-sm ${
|
|
||||||
currentStep === 'connect'
|
|
||||||
? isDarkMode
|
|
||||||
? 'text-zinc-300'
|
|
||||||
: 'text-zinc-700'
|
|
||||||
: isDarkMode
|
|
||||||
? 'text-zinc-500'
|
|
||||||
: 'text-zinc-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Connect and import a GitHub repo
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -154,71 +104,29 @@ export default function CreateProjectFlow() {
|
|||||||
onClick={() => navigateToStep('configure')}
|
onClick={() => navigateToStep('configure')}
|
||||||
>
|
>
|
||||||
<div className="mr-4">
|
<div className="mr-4">
|
||||||
<div
|
<div className={`w-10 h-10 rounded-lg ${currentStep === 'configure'
|
||||||
className={`w-10 h-10 rounded-lg ${
|
? (isDarkMode ? 'bg-white' : 'bg-black')
|
||||||
currentStep === 'configure'
|
: (isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200')
|
||||||
? isDarkMode
|
} flex items-center justify-center`}>
|
||||||
? 'bg-white'
|
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className={currentStep === 'configure'
|
||||||
: 'bg-black'
|
? (isDarkMode ? 'text-black' : 'text-white')
|
||||||
: isDarkMode
|
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
|
||||||
? 'bg-zinc-800'
|
}>
|
||||||
: 'bg-zinc-200'
|
|
||||||
} flex items-center justify-center`}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
fill="none"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className={
|
|
||||||
currentStep === 'configure'
|
|
||||||
? isDarkMode
|
|
||||||
? 'text-black'
|
|
||||||
: 'text-white'
|
|
||||||
: isDarkMode
|
|
||||||
? 'text-zinc-400'
|
|
||||||
: 'text-zinc-600'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<path d="M12 20h9"></path>
|
<path d="M12 20h9"></path>
|
||||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className={`w-px h-10 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'} mx-auto mt-2`}></div>
|
||||||
className={`w-px h-10 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'} mx-auto mt-2`}
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3
|
<h3 className={`font-medium text-base ${currentStep === 'configure'
|
||||||
className={`font-medium text-base ${
|
? (isDarkMode ? 'text-white' : 'text-zinc-900')
|
||||||
currentStep === 'configure'
|
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
|
||||||
? isDarkMode
|
}`}>Configure</h3>
|
||||||
? 'text-white'
|
<p className={`text-sm ${currentStep === 'configure'
|
||||||
: 'text-zinc-900'
|
? (isDarkMode ? 'text-zinc-300' : 'text-zinc-700')
|
||||||
: isDarkMode
|
: (isDarkMode ? 'text-zinc-500' : 'text-zinc-500')
|
||||||
? 'text-zinc-400'
|
}`}>Define the deployment type</p>
|
||||||
: 'text-zinc-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Configure
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
className={`text-sm ${
|
|
||||||
currentStep === 'configure'
|
|
||||||
? isDarkMode
|
|
||||||
? 'text-zinc-300'
|
|
||||||
: 'text-zinc-700'
|
|
||||||
: isDarkMode
|
|
||||||
? 'text-zinc-500'
|
|
||||||
: 'text-zinc-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Define the deployment type
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -228,36 +136,14 @@ export default function CreateProjectFlow() {
|
|||||||
onClick={() => navigateToStep('deploy')}
|
onClick={() => navigateToStep('deploy')}
|
||||||
>
|
>
|
||||||
<div className="mr-4">
|
<div className="mr-4">
|
||||||
<div
|
<div className={`w-10 h-10 rounded-lg ${currentStep === 'deploy' || currentStep === 'success'
|
||||||
className={`w-10 h-10 rounded-lg ${
|
? (isDarkMode ? 'bg-white' : 'bg-black')
|
||||||
currentStep === 'deploy' || currentStep === 'success'
|
: (isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200')
|
||||||
? isDarkMode
|
} flex items-center justify-center`}>
|
||||||
? 'bg-white'
|
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className={currentStep === 'deploy' || currentStep === 'success'
|
||||||
: 'bg-black'
|
? (isDarkMode ? 'text-black' : 'text-white')
|
||||||
: isDarkMode
|
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
|
||||||
? 'bg-zinc-800'
|
}>
|
||||||
: 'bg-zinc-200'
|
|
||||||
} flex items-center justify-center`}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
fill="none"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className={
|
|
||||||
currentStep === 'deploy' || currentStep === 'success'
|
|
||||||
? isDarkMode
|
|
||||||
? 'text-black'
|
|
||||||
: 'text-white'
|
|
||||||
: isDarkMode
|
|
||||||
? 'text-zinc-400'
|
|
||||||
: 'text-zinc-600'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||||
<polyline points="7.5 4.21 12 6.81 16.5 4.21"></polyline>
|
<polyline points="7.5 4.21 12 6.81 16.5 4.21"></polyline>
|
||||||
<polyline points="7.5 19.79 7.5 14.6 3 12"></polyline>
|
<polyline points="7.5 19.79 7.5 14.6 3 12"></polyline>
|
||||||
@ -268,48 +154,26 @@ export default function CreateProjectFlow() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3
|
<h3 className={`font-medium text-base ${currentStep === 'deploy' || currentStep === 'success'
|
||||||
className={`font-medium text-base ${
|
? (isDarkMode ? 'text-white' : 'text-zinc-900')
|
||||||
currentStep === 'deploy' || currentStep === 'success'
|
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
|
||||||
? isDarkMode
|
}`}>Deploy</h3>
|
||||||
? 'text-white'
|
<p className={`text-sm ${currentStep === 'deploy' || currentStep === 'success'
|
||||||
: 'text-zinc-900'
|
? (isDarkMode ? 'text-zinc-300' : 'text-zinc-700')
|
||||||
: isDarkMode
|
: (isDarkMode ? 'text-zinc-500' : 'text-zinc-500')
|
||||||
? 'text-zinc-400'
|
}`}>Review and confirm deployment</p>
|
||||||
: 'text-zinc-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
className={`text-sm ${
|
|
||||||
currentStep === 'deploy' || currentStep === 'success'
|
|
||||||
? isDarkMode
|
|
||||||
? 'text-zinc-300'
|
|
||||||
: 'text-zinc-700'
|
|
||||||
: isDarkMode
|
|
||||||
? 'text-zinc-500'
|
|
||||||
: 'text-zinc-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Review and confirm deployment
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Laconic mark (larger, bottom left) */}
|
{/* Laconic mark (larger, bottom left) */}
|
||||||
<div className="absolute -bottom-2 -left-2 opacity-10">
|
<div className="absolute -bottom-2 -left-2 opacity-10">
|
||||||
<LaconicMark
|
<LaconicMark className={`w-40 h-40 ${isDarkMode ? 'text-zinc-300' : 'text-zinc-700'}`} />
|
||||||
className={`w-40 h-40 ${isDarkMode ? 'text-zinc-300' : 'text-zinc-700'}`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main content with fixed dimensions and scrolling */}
|
{/* Main content with fixed dimensions and scrolling */}
|
||||||
<div
|
<div className={`flex-1 ${isDarkMode ? 'bg-black' : 'bg-white'} relative`}>
|
||||||
className={`flex-1 ${isDarkMode ? 'bg-black' : 'bg-white'} relative`}
|
|
||||||
>
|
|
||||||
{/* Close button */}
|
{/* Close button */}
|
||||||
<button
|
<button
|
||||||
className={`absolute top-4 right-4 ${isDarkMode ? 'text-zinc-400 hover:text-white' : 'text-zinc-600 hover:text-zinc-900'} z-10`}
|
className={`absolute top-4 right-4 ${isDarkMode ? 'text-zinc-400 hover:text-white' : 'text-zinc-600 hover:text-zinc-900'} z-10`}
|
||||||
@ -328,15 +192,9 @@ export default function CreateProjectFlow() {
|
|||||||
|
|
||||||
{/* Progress indicator */}
|
{/* Progress indicator */}
|
||||||
<div className="absolute bottom-6 left-0 right-0 flex justify-center gap-3">
|
<div className="absolute bottom-6 left-0 right-0 flex justify-center gap-3">
|
||||||
<div
|
<div className={`w-12 h-1 rounded-full ${currentStep === 'connect' ? 'bg-blue-600' : (isDarkMode ? 'bg-zinc-700' : 'bg-zinc-300')}`}></div>
|
||||||
className={`w-12 h-1 rounded-full ${currentStep === 'connect' ? 'bg-blue-600' : isDarkMode ? 'bg-zinc-700' : 'bg-zinc-300'}`}
|
<div className={`w-12 h-1 rounded-full ${currentStep === 'configure' ? 'bg-blue-600' : (isDarkMode ? 'bg-zinc-700' : 'bg-zinc-300')}`}></div>
|
||||||
></div>
|
<div className={`w-12 h-1 rounded-full ${currentStep === 'deploy' || currentStep === 'success' ? 'bg-blue-600' : (isDarkMode ? 'bg-zinc-700' : 'bg-zinc-300')}`}></div>
|
||||||
<div
|
|
||||||
className={`w-12 h-1 rounded-full ${currentStep === 'configure' ? 'bg-blue-600' : isDarkMode ? 'bg-zinc-700' : 'bg-zinc-300'}`}
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
className={`w-12 h-1 rounded-full ${currentStep === 'deploy' || currentStep === 'success' ? 'bg-blue-600' : isDarkMode ? 'bg-zinc-700' : 'bg-zinc-300'}`}
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export default function DeploymentsPage() {
|
|||||||
|
|
||||||
// State for deployment logs modal
|
// State for deployment logs modal
|
||||||
const [isLogsOpen, setIsLogsOpen] = useState(false);
|
const [isLogsOpen, setIsLogsOpen] = useState(false);
|
||||||
const [, setSelectedDeploymentId] = useState<string | null>(null);
|
const [selectedDeploymentId, setSelectedDeploymentId] = useState<string | null>(null);
|
||||||
const [deploymentLogs, setDeploymentLogs] = useState<string>('');
|
const [deploymentLogs, setDeploymentLogs] = useState<string>('');
|
||||||
|
|
||||||
// Create a default deployment
|
// Create a default deployment
|
||||||
@ -48,6 +48,19 @@ export default function DeploymentsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const secondDeployment: Deployment = {
|
||||||
|
id: 'previous',
|
||||||
|
branch: 'feature/new-ui',
|
||||||
|
status: 'COMPLETED',
|
||||||
|
isCurrent: false,
|
||||||
|
createdAt: Date.now() - 3 * 24 * 60 * 60 * 1000, // 3 days ago
|
||||||
|
applicationDeploymentRecordData: {
|
||||||
|
url: repoData ? `https://dev.${repoData.name.toLowerCase()}.example.com` : 'https://dev.example.com'
|
||||||
|
},
|
||||||
|
createdBy: {
|
||||||
|
name: repoData?.owner?.login || 'username'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize with empty data for testing the empty state
|
// Initialize with empty data for testing the empty state
|
||||||
// Comment this out to see the mock deployments
|
// Comment this out to see the mock deployments
|
||||||
@ -66,9 +79,7 @@ export default function DeploymentsPage() {
|
|||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
name: repoData ? `${repoData.name.toLowerCase()}.example.com` : 'example.com',
|
name: repoData ? `${repoData.name.toLowerCase()}.example.com` : 'example.com',
|
||||||
branch: 'main', // Add missing branch
|
|
||||||
status: 'ACTIVE',
|
status: 'ACTIVE',
|
||||||
createdAt: Date.now(), // Add missing createdAt
|
|
||||||
isCustom: false
|
isCustom: false
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@ -3,7 +3,12 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay";
|
import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay";
|
||||||
import { PlusIcon, ChevronDownIcon, ChevronUpIcon, PencilIcon, TrashIcon } from "lucide-react";
|
import { PlusIcon, ChevronDownIcon, ChevronUpIcon, PencilIcon, TrashIcon } from "lucide-react";
|
||||||
import type { EnvVarItem } from '@/types'
|
|
||||||
|
interface EnvVarItem {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
isEditing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface EnvGroupProps {
|
interface EnvGroupProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -199,7 +204,7 @@ export default function EnvVarsPage() {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-accent hover:text-accent-foreground rounded-md text-foreground transition-colors"
|
className="p-2 hover:bg-accent hover:text-accent-foreground rounded-md text-foreground transition-colors"
|
||||||
onClick={() => updateVariable(env, index, variable.key || '', variable.value || '')}
|
onClick={() => updateVariable(env, index, variable.key, variable.value)}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -278,9 +278,11 @@ export default function ProjectSettingsPage({ project, onProjectUpdated }: Proje
|
|||||||
Select account
|
Select account
|
||||||
</Label>
|
</Label>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
placeholder="Select"
|
label="Select"
|
||||||
options={accountOptions}
|
options={accountOptions}
|
||||||
onChange={(option) => setSelectedAccount(option.value)}
|
selectedValue={selectedAccount}
|
||||||
|
onSelect={(value) => setSelectedAccount(value)}
|
||||||
|
className="w-full mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,159 @@
|
|||||||
|
'use client'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { PageWrapper } from '@/components/foundation'
|
||||||
|
import { FixedProjectCard } from '@/components/projects/project/ProjectCard/FixedProjectCard'
|
||||||
|
import { Button } from '@workspace/ui/components/button'
|
||||||
|
import { Shapes } from 'lucide-react'
|
||||||
|
import { useAuth } from '@clerk/nextjs'
|
||||||
|
|
||||||
|
interface Deployment {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
repositoryId: string
|
||||||
|
status: 'running' | 'complete' | 'failed'
|
||||||
|
url?: string
|
||||||
|
branch: string
|
||||||
|
createdAt: string
|
||||||
|
createdBy: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectsPage() {
|
||||||
|
const [deployments, setDeployments] = useState<Deployment[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const { isLoaded: isAuthLoaded, userId } = useAuth()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchDeployments() {
|
||||||
|
if (!isAuthLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!userId) {
|
||||||
|
setError('Not authenticated');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a real implementation, you would query your GraphQL backend
|
||||||
|
// For now, we'll mock some deployments
|
||||||
|
const mockDeployments: Deployment[] = [
|
||||||
|
{
|
||||||
|
id: 'dep_abc123',
|
||||||
|
name: 'My Project',
|
||||||
|
repositoryId: '123456',
|
||||||
|
status: 'complete',
|
||||||
|
url: 'https://my-project.example.com',
|
||||||
|
branch: 'main',
|
||||||
|
createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
createdBy: {
|
||||||
|
name: 'John Doe'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dep_def456',
|
||||||
|
name: 'Another Project',
|
||||||
|
repositoryId: '789012',
|
||||||
|
status: 'running',
|
||||||
|
branch: 'develop',
|
||||||
|
createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
|
||||||
|
createdBy: {
|
||||||
|
name: 'Jane Smith'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
setDeployments(mockDeployments);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching deployments:', err)
|
||||||
|
setError('Failed to fetch deployments')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDeployments()
|
||||||
|
}, [isAuthLoaded, userId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper
|
||||||
|
header={{
|
||||||
|
title: 'Projects',
|
||||||
|
actions: [{ label: 'Create Project', href: '/projects/create' }]
|
||||||
|
}}
|
||||||
|
layout="bento"
|
||||||
|
className="pb-0"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="md:col-span-3 flex justify-center items-center min-h-[600px]">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="md:col-span-3 border border-gray-800 rounded-lg min-h-[600px] flex flex-col items-center justify-center p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<Shapes size={64} className="stroke-current" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Error: {error}</h2>
|
||||||
|
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||||
|
There was an error loading your deployments.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
className="bg-white text-black hover:bg-gray-200"
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : deployments.length === 0 ? (
|
||||||
|
<div className="md:col-span-3 border border-gray-800 rounded-lg min-h-[600px] flex flex-col items-center justify-center p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<Shapes size={64} className="stroke-current" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Deploy your first app</h2>
|
||||||
|
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||||
|
You haven't deployed any projects yet. Start by importing a repository from your GitHub account.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
className="bg-white text-black hover:bg-gray-200"
|
||||||
|
onClick={() => window.location.href = '/projects/create'}
|
||||||
|
>
|
||||||
|
Create Project
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="md:col-span-3">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{deployments.map((deployment) => (
|
||||||
|
<FixedProjectCard
|
||||||
|
project={{
|
||||||
|
id: deployment.id,
|
||||||
|
name: deployment.name,
|
||||||
|
deployments: [{
|
||||||
|
applicationDeploymentRecordData: {
|
||||||
|
url: deployment.url
|
||||||
|
},
|
||||||
|
branch: deployment.branch,
|
||||||
|
createdAt: deployment.createdAt,
|
||||||
|
createdBy: deployment.createdBy
|
||||||
|
}]
|
||||||
|
}}
|
||||||
|
key={deployment.id}
|
||||||
|
status={deployment.status === 'complete' ? 'success' :
|
||||||
|
deployment.status === 'running' ? 'pending' : 'error'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PageWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -42,7 +42,6 @@ export default function ProjectOverviewPage() {
|
|||||||
const [deployments, setDeployments] = useState<any[]>([]);
|
const [deployments, setDeployments] = useState<any[]>([]);
|
||||||
const [filteredDeployments, setFilteredDeployments] = useState<any[]>([]);
|
const [filteredDeployments, setFilteredDeployments] = useState<any[]>([]);
|
||||||
const [isLogsOpen, setIsLogsOpen] = useState(false);
|
const [isLogsOpen, setIsLogsOpen] = useState(false);
|
||||||
const [, setSelectedDeploymentId] = useState<string | null>(null);
|
|
||||||
const [deploymentLogs, setDeploymentLogs] = useState<string>('');
|
const [deploymentLogs, setDeploymentLogs] = useState<string>('');
|
||||||
|
|
||||||
// Load project data
|
// Load project data
|
||||||
@ -139,25 +138,18 @@ export default function ProjectOverviewPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle deployment logs
|
// Handle deployment logs
|
||||||
const handleViewLogs = (deploymentId: string) => {
|
const handleViewLogs = () => {
|
||||||
setSelectedDeploymentId(deploymentId);
|
const mockLogs = `[2025-02-12 10:03:12] INFO Starting deployment process for service: ${project?.name}
|
||||||
|
[2025-02-12 10:03:14] INFO Fetching latest commit from main branch (commit: a1b2c3d)
|
||||||
// Mock logs data
|
[2025-02-12 10:03:15] INFO Building Docker image: registry.company.com/${project?.name}:latest
|
||||||
const mockLogs = `[2025-02-12 10:03:12] INFO Starting deployment process for service: api-gateway
|
[2025-02-12 10:03:26] INFO Running security scan on built image
|
||||||
[2025-02-12 10:03:14] INFO Fetching latest commit from main branch (commit: a1b2c3d)
|
[2025-02-12 10:03:30] INFO Pushing image to container registry
|
||||||
[2025-02-12 10:03:15] INFO Building Docker image: registry.company.com/api-gateway:latest
|
[2025-02-12 10:03:35] INFO Updating deployment configuration
|
||||||
[2025-02-12 10:03:26] INFO Running security scan on built image
|
[2025-02-12 10:03:40] INFO Scaling down old pods
|
||||||
[2025-02-12 10:03:27] WARNING Medium severity vulnerability detected in package 'openssl'
|
[2025-02-12 10:03:42] INFO Scaling up new pods
|
||||||
[2025-02-12 10:03:30] INFO Pushing image to container registry
|
[2025-02-12 10:03:50] INFO Running health checks on new pods
|
||||||
[2025-02-12 10:03:35] INFO Updating Kubernetes deployment
|
[2025-02-12 10:03:55] INFO Deployment completed successfully
|
||||||
[2025-02-12 10:03:40] INFO Scaling down old pods
|
[2025-02-12 10:03:56] INFO Service is now live at ${currentDeployment?.applicationDeploymentRecordData?.url}`;
|
||||||
[2025-02-12 10:03:42] INFO Scaling up new pods
|
|
||||||
[2025-02-12 10:03:50] INFO Running health checks on new pods
|
|
||||||
[2025-02-12 10:03:52] ERROR Pod 'api-gateway-7df9bbb500-tx2k4' failed readiness probe (502 Bad Gateway)
|
|
||||||
[2025-02-12 10:03:55] INFO Retrying deployment with previous stable image
|
|
||||||
[2025-02-12 10:04:03] INFO Rolling back to registry.company.com/api-gateway:previous
|
|
||||||
[2025-02-12 10:04:10] INFO Deployment rolled back successfully
|
|
||||||
[2025-02-12 10:04:11] ERROR Deployment failed, please review logs and fix errors`;
|
|
||||||
|
|
||||||
setDeploymentLogs(mockLogs);
|
setDeploymentLogs(mockLogs);
|
||||||
setIsLogsOpen(true);
|
setIsLogsOpen(true);
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { Providers } from '@/components/providers'
|
import { Providers } from '@/components/providers'
|
||||||
import { ClerkProvider } from '@clerk/nextjs'
|
import { ClerkProvider } from '@clerk/nextjs'
|
||||||
import '@workspace/ui/globals.css'
|
import '@workspace/ui/globals.css'
|
||||||
import { AutoSignInIFrameModal } from '@/components/iframe/auto-sign-in'
|
|
||||||
import { CheckBalanceWrapper } from '@/components/iframe/check-balance-iframe/CheckBalanceWrapper'
|
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
|
import { CheckBalanceWrapper } from '@/components/iframe/check-balance-iframe/CheckBalanceWrapper'
|
||||||
|
import { AutoSignInIFrameModal } from '@/components/iframe/auto-sign-in'
|
||||||
|
|
||||||
// Add root metadata with template pattern
|
// Add root metadata with template pattern
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@ -25,10 +25,7 @@ export default function RootLayout({
|
|||||||
children
|
children
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
return (
|
return (
|
||||||
<ClerkProvider
|
<ClerkProvider signInFallbackRedirectUrl="/home">
|
||||||
publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
|
|
||||||
signInFallbackRedirectUrl="/home"
|
|
||||||
>
|
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={`${inter.className} `} suppressHydrationWarning>
|
<body className={`${inter.className} `} suppressHydrationWarning>
|
||||||
<main>
|
<main>
|
||||||
|
|||||||
@ -27,8 +27,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
|
||||||
|
|
||||||
export default function TestConnectionPage() {
|
export default function TestConnectionPage() {
|
||||||
// Get getToken from useAuth hook, not from user
|
// Get getToken from useAuth hook, not from user
|
||||||
const { isSignedIn, isLoaded: isClerkLoaded, getToken } = useAuth()
|
const { isSignedIn, isLoaded: isClerkLoaded, getToken } = useAuth()
|
||||||
@ -138,7 +136,7 @@ export default function TestConnectionPage() {
|
|||||||
const checkBackendConnection = async () => {
|
const checkBackendConnection = async () => {
|
||||||
try {
|
try {
|
||||||
// Test session
|
// Test session
|
||||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
@ -158,7 +156,7 @@ export default function TestConnectionPage() {
|
|||||||
|
|
||||||
// Set default org if available
|
// Set default org if available
|
||||||
if (orgsData.organizations && orgsData.organizations.length > 0) {
|
if (orgsData.organizations && orgsData.organizations.length > 0) {
|
||||||
setSelectedOrg(orgsData.organizations[0]!.slug)
|
setSelectedOrg(orgsData.organizations[0].slug)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching organizations:', error)
|
console.error('Error fetching organizations:', error)
|
||||||
@ -194,7 +192,7 @@ export default function TestConnectionPage() {
|
|||||||
const checkWalletConnection = async () => {
|
const checkWalletConnection = async () => {
|
||||||
if (isBackendConnected) {
|
if (isBackendConnected) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
@ -232,7 +230,7 @@ const fetchDeployers = async () => {
|
|||||||
|
|
||||||
// Auto-select first deployer if available
|
// Auto-select first deployer if available
|
||||||
if (deployersData.deployers && deployersData.deployers.length > 0) {
|
if (deployersData.deployers && deployersData.deployers.length > 0) {
|
||||||
setSelectedDeployer(deployersData.deployers[0]!.deployerLrn)
|
setSelectedDeployer(deployersData.deployers[0].deployerLrn)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching deployers:', error)
|
console.error('Error fetching deployers:', error)
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import { Button } from '@workspace/ui/components/button'
|
|||||||
import { CheckBalanceWrapper } from './iframe/check-balance-iframe/CheckBalanceWrapper'
|
import { CheckBalanceWrapper } from './iframe/check-balance-iframe/CheckBalanceWrapper'
|
||||||
import { CopyIcon } from 'lucide-react'
|
import { CopyIcon } from 'lucide-react'
|
||||||
|
|
||||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
|
||||||
|
|
||||||
// Generate a random nonce
|
// Generate a random nonce
|
||||||
function generateNonce() {
|
function generateNonce() {
|
||||||
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
||||||
@ -27,7 +25,7 @@ export function SIWEAuth() {
|
|||||||
const checkSession = async () => {
|
const checkSession = async () => {
|
||||||
try {
|
try {
|
||||||
setSessionStatus('checking')
|
setSessionStatus('checking')
|
||||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
@ -173,7 +171,7 @@ Issued At: ${issuedAt}`
|
|||||||
setDebugInfo(prev => `${prev}\nRaw signature: ${signedMessage}`)
|
setDebugInfo(prev => `${prev}\nRaw signature: ${signedMessage}`)
|
||||||
|
|
||||||
// Try using the raw signature directly
|
// Try using the raw signature directly
|
||||||
const response = await fetch(`${BACKEND_URL}/auth/validate`, {
|
const response = await fetch('http://localhost:8000/auth/validate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -196,7 +194,7 @@ Issued At: ${issuedAt}`
|
|||||||
setDebugInfo(prev => `${prev}\nValidation response: ${JSON.stringify(responseData)}`)
|
setDebugInfo(prev => `${prev}\nValidation response: ${JSON.stringify(responseData)}`)
|
||||||
|
|
||||||
// If successful, we're done
|
// If successful, we're done
|
||||||
if (response.ok && (responseData as any).success) {
|
if (response.ok && responseData.success) {
|
||||||
console.log('Authentication successful!')
|
console.log('Authentication successful!')
|
||||||
setDebugInfo(prev => `${prev}\nAuthentication successful!`)
|
setDebugInfo(prev => `${prev}\nAuthentication successful!`)
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import { useState, useEffect } from 'react'
|
|||||||
import { Button } from '@workspace/ui/components/button'
|
import { Button } from '@workspace/ui/components/button'
|
||||||
import { Wallet } from 'ethers' // Add this to your package.json if not already there
|
import { Wallet } from 'ethers' // Add this to your package.json if not already there
|
||||||
|
|
||||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
|
||||||
|
|
||||||
export function DirectKeyAuth() {
|
export function DirectKeyAuth() {
|
||||||
const [sessionStatus, setSessionStatus] = useState<'checking' | 'authenticated' | 'unauthenticated'>('checking')
|
const [sessionStatus, setSessionStatus] = useState<'checking' | 'authenticated' | 'unauthenticated'>('checking')
|
||||||
const [sessionData, setSessionData] = useState<any>(null)
|
const [sessionData, setSessionData] = useState<any>(null)
|
||||||
@ -16,7 +14,7 @@ export function DirectKeyAuth() {
|
|||||||
const checkSession = async () => {
|
const checkSession = async () => {
|
||||||
try {
|
try {
|
||||||
setSessionStatus('checking')
|
setSessionStatus('checking')
|
||||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
@ -81,7 +79,7 @@ Issued At: ${issuedAt}`;
|
|||||||
console.log('Generated signature:', signature);
|
console.log('Generated signature:', signature);
|
||||||
|
|
||||||
// Send to backend
|
// Send to backend
|
||||||
const response = await fetch(`${BACKEND_URL}/auth/validate`, {
|
const response = await fetch('http://localhost:8000/auth/validate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useGQLClient } from '@/context'
|
import { useGQLClient } from '@/context'
|
||||||
|
|
||||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
|
||||||
|
|
||||||
export function GQLTest() {
|
export function GQLTest() {
|
||||||
const [testResponse, setTestResponse] = useState<string>('Testing connection...')
|
const [testResponse, setTestResponse] = useState<string>('Testing connection...')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@ -13,7 +11,7 @@ export function GQLTest() {
|
|||||||
async function testGQLConnection() {
|
async function testGQLConnection() {
|
||||||
try {
|
try {
|
||||||
// Try a direct GraphQL query using fetch
|
// Try a direct GraphQL query using fetch
|
||||||
const response = await fetch(`${BACKEND_URL}/graphql`, {
|
const response = await fetch('http://localhost:8000/graphql', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -52,7 +50,7 @@ export function GQLTest() {
|
|||||||
setTestResponse('Testing direct connection...')
|
setTestResponse('Testing direct connection...')
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -8,10 +8,9 @@ import {
|
|||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@workspace/ui/components/dropdown-menu'
|
} from '@workspace/ui/components/dropdown-menu'
|
||||||
import { cn } from '@workspace/ui/lib/utils'
|
import { cn } from '@workspace/ui/lib/utils'
|
||||||
import { MoreVertical, ExternalLink } from 'lucide-react'
|
import { MoreVertical } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import type { LucideIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for header action buttons/links
|
* Configuration for header action buttons/links
|
||||||
@ -20,7 +19,6 @@ import type { LucideIcon } from 'lucide-react'
|
|||||||
* - Use onClick for JS actions OR href for navigation (not both)
|
* - Use onClick for JS actions OR href for navigation (not both)
|
||||||
* - Multiple visual styles via variant prop
|
* - Multiple visual styles via variant prop
|
||||||
* - Optional primary emphasis for main call-to-action
|
* - Optional primary emphasis for main call-to-action
|
||||||
* - Optional icon support
|
|
||||||
*/
|
*/
|
||||||
export interface PageAction {
|
export interface PageAction {
|
||||||
/**
|
/**
|
||||||
@ -29,23 +27,6 @@ export interface PageAction {
|
|||||||
*/
|
*/
|
||||||
label: string
|
label: string
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional icon for the action
|
|
||||||
* @remarks
|
|
||||||
* - Can be string identifier or Lucide icon component
|
|
||||||
* - Common values: 'external-link', 'plus', 'edit', etc.
|
|
||||||
* - Displayed alongside the label
|
|
||||||
*/
|
|
||||||
icon?: string | LucideIcon
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this is an external link
|
|
||||||
* @remarks
|
|
||||||
* - When true, opens in new tab/window
|
|
||||||
* - Automatically adds external link icon if no icon specified
|
|
||||||
*/
|
|
||||||
external?: boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visual style variant for the button
|
* Visual style variant for the button
|
||||||
* @remarks
|
* @remarks
|
||||||
@ -131,14 +112,6 @@ export interface PageHeaderProps {
|
|||||||
*/
|
*/
|
||||||
subtitle?: string | ReactNode
|
subtitle?: string | ReactNode
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional description text
|
|
||||||
* @remarks
|
|
||||||
* - Displayed below subtitle
|
|
||||||
* - Useful for longer explanatory text
|
|
||||||
*/
|
|
||||||
description?: string
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array of action buttons/links
|
* Array of action buttons/links
|
||||||
* @remarks
|
* @remarks
|
||||||
@ -146,7 +119,6 @@ export interface PageHeaderProps {
|
|||||||
* - Mobile: Primary actions shown, secondary in dropdown
|
* - Mobile: Primary actions shown, secondary in dropdown
|
||||||
* - Actions can be buttons (onClick) or links (href)
|
* - Actions can be buttons (onClick) or links (href)
|
||||||
* - Support multiple visual styles via variant prop
|
* - Support multiple visual styles via variant prop
|
||||||
* - Support icons and external links
|
|
||||||
*
|
*
|
||||||
* @see {@link PageAction} for detailed action configuration
|
* @see {@link PageAction} for detailed action configuration
|
||||||
*
|
*
|
||||||
@ -156,15 +128,12 @@ export interface PageHeaderProps {
|
|||||||
* {
|
* {
|
||||||
* label: "Create New",
|
* label: "Create New",
|
||||||
* isPrimary: true,
|
* isPrimary: true,
|
||||||
* icon: "plus",
|
|
||||||
* onClick: () => setOpen(true)
|
* onClick: () => setOpen(true)
|
||||||
* },
|
* },
|
||||||
* {
|
* {
|
||||||
* label: "View All",
|
* label: "View All",
|
||||||
* href: "/items",
|
* href: "/items",
|
||||||
* variant: "outline",
|
* variant: "outline"
|
||||||
* icon: "external-link",
|
|
||||||
* external: true
|
|
||||||
* }
|
* }
|
||||||
* ]}
|
* ]}
|
||||||
* ```
|
* ```
|
||||||
@ -183,11 +152,102 @@ export interface PageHeaderProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A responsive page header component with title, subtitle, and actions.
|
* A responsive page header component with title, subtitle, and actions.
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* PageHeader provides a consistent header structure with:
|
||||||
|
* - Prominent title as h1
|
||||||
|
* - Optional subtitle or custom component
|
||||||
|
* - Configurable action buttons/links
|
||||||
|
* - Responsive layout with mobile optimization
|
||||||
|
* - Customizable styling
|
||||||
|
*
|
||||||
|
* @keywords header, page-title, action-buttons, responsive-header, foundation-component
|
||||||
|
* @category Layout
|
||||||
|
* @scope Foundation
|
||||||
|
*
|
||||||
|
* @usage
|
||||||
|
* Common patterns:
|
||||||
|
*
|
||||||
|
* Basic title only:
|
||||||
|
* ```tsx
|
||||||
|
* <PageHeader title="Dashboard" />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* With subtitle and primary action:
|
||||||
|
* ```tsx
|
||||||
|
* <PageHeader
|
||||||
|
* title="Projects"
|
||||||
|
* subtitle="Your active projects"
|
||||||
|
* actions={[
|
||||||
|
* { label: "New Project", isPrimary: true, onClick: handleCreate }
|
||||||
|
* ]}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* With search component and multiple actions:
|
||||||
|
* ```tsx
|
||||||
|
* <PageHeader
|
||||||
|
* title="Team Members"
|
||||||
|
* subtitle={<SearchInput placeholder="Search members..." />}
|
||||||
|
* actions={[
|
||||||
|
* { label: "Invite", isPrimary: true, onClick: handleInvite },
|
||||||
|
* { label: "Export", variant: "outline", onClick: handleExport },
|
||||||
|
* { label: "Settings", href: "/team/settings", variant: "ghost" }
|
||||||
|
* ]}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* With navigation actions:
|
||||||
|
* ```tsx
|
||||||
|
* <PageHeader
|
||||||
|
* title="Edit Profile"
|
||||||
|
* actions={[
|
||||||
|
* { label: "Save", isPrimary: true, onClick: handleSave },
|
||||||
|
* { label: "Cancel", href: "/profile", variant: "ghost" }
|
||||||
|
* ]}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <PageHeader
|
||||||
|
* title="Dashboard"
|
||||||
|
* subtitle="Welcome back!"
|
||||||
|
* actions={[
|
||||||
|
* {
|
||||||
|
* label: "New Item",
|
||||||
|
* isPrimary: true,
|
||||||
|
* onClick: () => console.log("clicked")
|
||||||
|
* }
|
||||||
|
* ]}
|
||||||
|
* className="mb-8"
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @related {@link PageWrapper} - Often used together for page layout
|
||||||
|
* @related {@link Button} - Used for rendering actions
|
||||||
|
* @composition Uses {@link DropdownMenu} for mobile action menu
|
||||||
|
*
|
||||||
|
* @cssUtilities
|
||||||
|
* - flex-col/flex-row: Responsive layout
|
||||||
|
* - gap-6/gap-2: Consistent spacing
|
||||||
|
* - text-2xl/text-[30px]: Responsive typography
|
||||||
|
* - text-foreground/text-muted-foreground: Text hierarchy
|
||||||
|
*
|
||||||
|
* @accessibility
|
||||||
|
* - Uses semantic h1 for title
|
||||||
|
* - Maintains text contrast ratios
|
||||||
|
* - Dropdown menu is keyboard navigable
|
||||||
|
* - Preserves action button/link semantics
|
||||||
|
*
|
||||||
|
* @performance
|
||||||
|
* - Conditional rendering of subtitle and actions
|
||||||
|
* - Mobile-first CSS with responsive modifiers
|
||||||
|
* - Efficient action rendering with key prop
|
||||||
*/
|
*/
|
||||||
export default function PageHeader({
|
export default function PageHeader({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
description,
|
|
||||||
actions = [],
|
actions = [],
|
||||||
className
|
className
|
||||||
}: PageHeaderProps) {
|
}: PageHeaderProps) {
|
||||||
@ -195,51 +255,21 @@ export default function PageHeader({
|
|||||||
const primaryActions = actions.filter((action) => action.isPrimary)
|
const primaryActions = actions.filter((action) => action.isPrimary)
|
||||||
const secondaryActions = actions.filter((action) => !action.isPrimary)
|
const secondaryActions = actions.filter((action) => !action.isPrimary)
|
||||||
|
|
||||||
// Get icon component from string or return the component directly
|
|
||||||
const getIconComponent = (icon: string | LucideIcon | undefined) => {
|
|
||||||
if (!icon) return null
|
|
||||||
|
|
||||||
if (typeof icon === 'string') {
|
|
||||||
switch (icon) {
|
|
||||||
case 'external-link':
|
|
||||||
return ExternalLink
|
|
||||||
default:
|
|
||||||
return ExternalLink // fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render an action (either as button or link)
|
// Render an action (either as button or link)
|
||||||
const renderAction = (action: PageAction, key: string) => {
|
const renderAction = (action: PageAction, key: string) => {
|
||||||
const variant = action.variant || (action.isPrimary ? 'default' : 'outline')
|
const variant = action.variant || (action.isPrimary ? 'default' : 'outline')
|
||||||
const IconComponent = getIconComponent(action.icon || (action.external ? 'external-link' : undefined))
|
|
||||||
|
|
||||||
const content = (
|
|
||||||
<>
|
|
||||||
{action.label}
|
|
||||||
{IconComponent && <IconComponent className="h-4 w-4 ml-2" />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (action.href) {
|
if (action.href) {
|
||||||
return (
|
return (
|
||||||
<Button key={key} variant={variant} asChild>
|
<Button key={key} variant={variant} asChild>
|
||||||
<Link
|
<Link href={action.href}>{action.label}</Link>
|
||||||
href={action.href}
|
|
||||||
target={action.external ? '_blank' : undefined}
|
|
||||||
rel={action.external ? 'noopener noreferrer' : undefined}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button key={key} variant={variant} onClick={action.onClick}>
|
<Button key={key} variant={variant} onClick={action.onClick}>
|
||||||
{content}
|
{action.label}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -262,11 +292,6 @@ export default function PageHeader({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{description && (
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{actions.length > 0 && (
|
{actions.length > 0 && (
|
||||||
@ -295,16 +320,7 @@ export default function PageHeader({
|
|||||||
{secondaryActions.map((action) =>
|
{secondaryActions.map((action) =>
|
||||||
action.href ? (
|
action.href ? (
|
||||||
<DropdownMenuItem asChild key={action.label}>
|
<DropdownMenuItem asChild key={action.label}>
|
||||||
<Link
|
<Link href={action.href}>{action.label}</Link>
|
||||||
href={action.href}
|
|
||||||
target={action.external ? '_blank' : undefined}
|
|
||||||
rel={action.external ? 'noopener noreferrer' : undefined}
|
|
||||||
>
|
|
||||||
{action.label}
|
|
||||||
{getIconComponent(action.icon || (action.external ? 'external-link' : undefined)) && (
|
|
||||||
<ExternalLink className="h-4 w-4 ml-2" />
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : (
|
) : (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useWallet } from '@/context/WalletContext'
|
import { useWallet } from '@/context/WalletContextProvider'
|
||||||
import { Button } from '@workspace/ui/components/button'
|
import { Button } from '@workspace/ui/components/button'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
import { ConfigureStep } from '@/components/onboarding/configure-step'
|
import { ConfigureStep } from '@/components/onboarding/configure-step'
|
||||||
import { ConnectStep } from '@/components/onboarding/connect-step'
|
import { ConnectStep } from '@/components/onboarding/connect-step'
|
||||||
import { DeployStep } from '@/components/onboarding/deploy-step'
|
import { DeployStep } from '@/components/onboarding/deploy-step'
|
||||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
import { useOnboarding } from '@/components/onboarding/store'
|
||||||
import { ScrollArea } from '@workspace/ui/components/scroll-area'
|
import { ScrollArea } from '@workspace/ui/components/scroll-area'
|
||||||
import { SidebarNav } from './sidebar'
|
import { SidebarNav } from './sidebar'
|
||||||
|
|
||||||
|
|||||||
@ -269,7 +269,7 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
|||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
onClose
|
onClose
|
||||||
}) => {
|
}) => {
|
||||||
const { nextStep, setFormData, formData, currentStep, previousStep } = useOnboarding()
|
const { nextStep, setFormData, formData, currentStep } = useOnboarding()
|
||||||
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
|
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
|
||||||
const [isImportMode, setIsImportMode] = useState(true)
|
const [isImportMode, setIsImportMode] = useState(true)
|
||||||
const [isOpen, setIsOpen] = useState(defaultOpen)
|
const [isOpen, setIsOpen] = useState(defaultOpen)
|
||||||
@ -489,7 +489,7 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
|
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
|
||||||
onClick={() => previousStep()}
|
onClick={() => useOnboarding.getState().previousStep()}
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
@ -527,7 +527,7 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
|
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
|
||||||
onClick={() => previousStep()}
|
onClick={() => useOnboarding.getState().previousStep()}
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,33 +1,21 @@
|
|||||||
// src/components/onboarding/configure-step/configure-step.tsx
|
// src/components/onboarding/configure-step/configure-step.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { PlusCircle, Loader2, AlertTriangle, Info } from 'lucide-react'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||||
import { useGQLClient } from '@/context'
|
import { useGQLClient } from '@/context'
|
||||||
import { useWallet } from '@/context/WalletContext'
|
import { useWallet } from '@/context/WalletContext'
|
||||||
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
|
||||||
import { Badge } from '@workspace/ui/components/badge'
|
|
||||||
import { Button } from '@workspace/ui/components/button'
|
import { Button } from '@workspace/ui/components/button'
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle
|
|
||||||
} from '@workspace/ui/components/card'
|
|
||||||
import { Checkbox } from '@workspace/ui/components/checkbox'
|
|
||||||
import { Input } from '@workspace/ui/components/input'
|
import { Input } from '@workspace/ui/components/input'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
|
||||||
|
import { Checkbox } from '@workspace/ui/components/checkbox'
|
||||||
import { Label } from '@workspace/ui/components/label'
|
import { Label } from '@workspace/ui/components/label'
|
||||||
import {
|
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||||
Select,
|
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||||
SelectContent,
|
import { Badge } from '@workspace/ui/components/badge'
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue
|
|
||||||
} from '@workspace/ui/components/select'
|
|
||||||
import { AlertTriangle, Info, Loader2, PlusCircle } from 'lucide-react'
|
|
||||||
import { useTheme } from 'next-themes'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { adaptDeployers } from '../../../utils/typeAdapters'
|
|
||||||
|
|
||||||
interface Deployer {
|
interface Deployer {
|
||||||
deployerLrn: string
|
deployerLrn: string
|
||||||
@ -55,21 +43,23 @@ export function ConfigureStep() {
|
|||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>(
|
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>(
|
||||||
(formData.deploymentType as 'auction' | 'lrn') || 'lrn' // Default to LRN for simplicity
|
formData.deploymentType as ('auction' | 'lrn') || 'lrn' // Default to LRN for simplicity
|
||||||
)
|
)
|
||||||
const [numberOfDeployers, setNumberOfDeployers] = useState<string>(
|
const [numberOfDeployers, setNumberOfDeployers] = useState<string>(
|
||||||
formData.deployerCount || '1'
|
formData.deployerCount || "1"
|
||||||
|
)
|
||||||
|
const [maxPrice, setMaxPrice] = useState<string>(
|
||||||
|
formData.maxPrice || "1000"
|
||||||
)
|
)
|
||||||
const [maxPrice, setMaxPrice] = useState<string>(formData.maxPrice || '1000')
|
|
||||||
const [selectedLrn, setSelectedLrn] = useState<string>(
|
const [selectedLrn, setSelectedLrn] = useState<string>(
|
||||||
formData.selectedLrn || ''
|
formData.selectedLrn || ""
|
||||||
)
|
)
|
||||||
const [selectedOrg, setSelectedOrg] = useState<string>(
|
const [selectedOrg, setSelectedOrg] = useState<string>(
|
||||||
formData.selectedOrg || ''
|
formData.selectedOrg || ""
|
||||||
)
|
)
|
||||||
const [envVars, setEnvVars] = useState<
|
const [envVars, setEnvVars] = useState<{ key: string; value: string; environments: string[] }[]>([
|
||||||
{ key: string; value: string; environments: string[] }[]
|
{ key: '', value: '', environments: ['Production'] }
|
||||||
>([{ key: '', value: '', environments: ['Production'] }])
|
])
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
const gqlClient = useGQLClient()
|
const gqlClient = useGQLClient()
|
||||||
@ -90,15 +80,10 @@ export function ConfigureStep() {
|
|||||||
|
|
||||||
// Initialize environment variables from formData if available
|
// Initialize environment variables from formData if available
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (formData.environmentVariables && Array.isArray(formData.environmentVariables)) {
|
||||||
formData.environmentVariables &&
|
setEnvVars(formData.environmentVariables.length > 0 ? formData.environmentVariables : [
|
||||||
Array.isArray(formData.environmentVariables)
|
{ key: '', value: '', environments: ['Production'] }
|
||||||
) {
|
])
|
||||||
setEnvVars(
|
|
||||||
formData.environmentVariables.length > 0
|
|
||||||
? formData.environmentVariables
|
|
||||||
: [{ key: '', value: '', environments: ['Production'] }]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}, [formData.environmentVariables])
|
}, [formData.environmentVariables])
|
||||||
|
|
||||||
@ -108,15 +93,11 @@ export function ConfigureStep() {
|
|||||||
setIsLoadingDeployers(true)
|
setIsLoadingDeployers(true)
|
||||||
const deployersData = await gqlClient.getDeployers()
|
const deployersData = await gqlClient.getDeployers()
|
||||||
console.log('Available deployers:', deployersData)
|
console.log('Available deployers:', deployersData)
|
||||||
setDeployers(adaptDeployers(deployersData.deployers || []))
|
setDeployers(deployersData.deployers || [])
|
||||||
|
|
||||||
// Auto-select first deployer if available and none selected
|
// Auto-select first deployer if available and none selected
|
||||||
if (
|
if (deployersData.deployers && deployersData.deployers.length > 0 && !selectedLrn) {
|
||||||
deployersData.deployers &&
|
setSelectedLrn(deployersData.deployers[0].deployerLrn)
|
||||||
deployersData.deployers.length > 0 &&
|
|
||||||
!selectedLrn
|
|
||||||
) {
|
|
||||||
setSelectedLrn(deployersData.deployers[0]!.deployerLrn)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching deployers:', error)
|
console.error('Error fetching deployers:', error)
|
||||||
@ -135,12 +116,8 @@ export function ConfigureStep() {
|
|||||||
setOrganizations(orgsData.organizations || [])
|
setOrganizations(orgsData.organizations || [])
|
||||||
|
|
||||||
// Auto-select first organization if available and none selected
|
// Auto-select first organization if available and none selected
|
||||||
if (
|
if (orgsData.organizations && orgsData.organizations.length > 0 && !selectedOrg) {
|
||||||
orgsData.organizations &&
|
setSelectedOrg(orgsData.organizations[0].slug)
|
||||||
orgsData.organizations.length > 0 &&
|
|
||||||
!selectedOrg
|
|
||||||
) {
|
|
||||||
setSelectedOrg(orgsData.organizations[0]!.slug)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching organizations:', error)
|
console.error('Error fetching organizations:', error)
|
||||||
@ -152,10 +129,7 @@ export function ConfigureStep() {
|
|||||||
|
|
||||||
// Add an empty environment variable row
|
// Add an empty environment variable row
|
||||||
const addEnvVar = () => {
|
const addEnvVar = () => {
|
||||||
setEnvVars([
|
setEnvVars([...envVars, { key: '', value: '', environments: ['Production'] }])
|
||||||
...envVars,
|
|
||||||
{ key: '', value: '', environments: ['Production'] }
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove environment variable row
|
// Remove environment variable row
|
||||||
@ -166,28 +140,19 @@ export function ConfigureStep() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update environment variable
|
// Update environment variable
|
||||||
const updateEnvVar = (
|
const updateEnvVar = (index: number, field: 'key' | 'value', value: string) => {
|
||||||
index: number,
|
|
||||||
field: 'key' | 'value',
|
|
||||||
value: string
|
|
||||||
) => {
|
|
||||||
const newEnvVars = [...envVars]
|
const newEnvVars = [...envVars]
|
||||||
if (newEnvVars[index]) {
|
|
||||||
newEnvVars[index][field] = value
|
newEnvVars[index][field] = value
|
||||||
}
|
|
||||||
setEnvVars(newEnvVars)
|
setEnvVars(newEnvVars)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle environment for variable
|
// Toggle environment for variable
|
||||||
const toggleEnvironment = (index: number, environment: string) => {
|
const toggleEnvironment = (index: number, environment: string) => {
|
||||||
const newEnvVars = [...envVars]
|
const newEnvVars = [...envVars]
|
||||||
if (newEnvVars[index]?.environments) {
|
|
||||||
const currentEnvs = newEnvVars[index].environments
|
const currentEnvs = newEnvVars[index].environments
|
||||||
|
|
||||||
if (currentEnvs.includes(environment)) {
|
if (currentEnvs.includes(environment)) {
|
||||||
newEnvVars[index].environments = currentEnvs.filter(
|
newEnvVars[index].environments = currentEnvs.filter(env => env !== environment)
|
||||||
(env) => env !== environment
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
newEnvVars[index].environments = [...currentEnvs, environment]
|
newEnvVars[index].environments = [...currentEnvs, environment]
|
||||||
}
|
}
|
||||||
@ -199,7 +164,6 @@ export function ConfigureStep() {
|
|||||||
|
|
||||||
setEnvVars(newEnvVars)
|
setEnvVars(newEnvVars)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle deployment option
|
// Toggle deployment option
|
||||||
const toggleDeployOption = (option: 'auction' | 'lrn') => {
|
const toggleDeployOption = (option: 'auction' | 'lrn') => {
|
||||||
@ -207,7 +171,7 @@ export function ConfigureStep() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get selected deployer details
|
// Get selected deployer details
|
||||||
const selectedDeployer = deployers.find((d) => d.deployerLrn === selectedLrn)
|
const selectedDeployer = deployers.find(d => d.deployerLrn === selectedLrn)
|
||||||
|
|
||||||
// Validate form
|
// Validate form
|
||||||
const canProceed = () => {
|
const canProceed = () => {
|
||||||
@ -225,9 +189,7 @@ export function ConfigureStep() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter out empty environment variables
|
// Filter out empty environment variables
|
||||||
const validEnvVars = envVars.filter(
|
const validEnvVars = envVars.filter(env => env.key.trim() && env.value.trim())
|
||||||
(env) => env.key.trim() && env.value.trim()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Save configuration to form data
|
// Save configuration to form data
|
||||||
setFormData({
|
setFormData({
|
||||||
@ -253,44 +215,19 @@ export function ConfigureStep() {
|
|||||||
|
|
||||||
// Get deployment mode info
|
// Get deployment mode info
|
||||||
const isTemplateMode = formData.deploymentMode === 'template'
|
const isTemplateMode = formData.deploymentMode === 'template'
|
||||||
const selectedItem = isTemplateMode
|
const selectedItem = isTemplateMode ? formData.template?.name : formData.githubRepo
|
||||||
? formData.template?.name
|
|
||||||
: formData.githubRepo
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col p-8 overflow-y-auto">
|
<div className="w-full h-full flex flex-col p-8 overflow-y-auto">
|
||||||
{/* Configure icon and header */}
|
{/* Configure icon and header */}
|
||||||
<div className="flex flex-col items-center justify-center mb-8">
|
<div className="flex flex-col items-center justify-center mb-8">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<svg
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={isDarkMode ? "text-white" : "text-black"}>
|
||||||
width="40"
|
<path d="M12 20h9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
height="40"
|
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className={isDarkMode ? 'text-white' : 'text-black'}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12 20h9"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2 className={`text-2xl font-medium text-center mb-2 ${isDarkMode ? "text-white" : "text-zinc-900"}`}>Configure</h2>
|
||||||
className={`text-2xl font-medium text-center mb-2 ${isDarkMode ? 'text-white' : 'text-zinc-900'}`}
|
|
||||||
>
|
|
||||||
Configure
|
|
||||||
</h2>
|
|
||||||
<p className={`text-center text-zinc-500 max-w-md`}>
|
<p className={`text-center text-zinc-500 max-w-md`}>
|
||||||
Define the deployment type
|
Define the deployment type
|
||||||
</p>
|
</p>
|
||||||
@ -309,9 +246,7 @@ export function ConfigureStep() {
|
|||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">Type:</span>
|
<span className="text-muted-foreground">Type:</span>
|
||||||
<Badge variant="secondary">
|
<Badge variant="secondary">{isTemplateMode ? 'Template' : 'Repository'}</Badge>
|
||||||
{isTemplateMode ? 'Template' : 'Repository'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">Source:</span>
|
<span className="text-muted-foreground">Source:</span>
|
||||||
@ -327,33 +262,24 @@ export function ConfigureStep() {
|
|||||||
|
|
||||||
{/* Organization Selection */}
|
{/* Organization Selection */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Label
|
<Label htmlFor="organization" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
htmlFor="organization"
|
|
||||||
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
|
||||||
>
|
|
||||||
Organization *
|
Organization *
|
||||||
</Label>
|
</Label>
|
||||||
{isLoadingOrgs ? (
|
{isLoadingOrgs ? (
|
||||||
<div className="flex items-center justify-center p-3 border rounded-md">
|
<div className="flex items-center justify-center p-3 border rounded-md">
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">Loading organizations...</span>
|
||||||
Loading organizations...
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : organizations.length === 0 ? (
|
) : organizations.length === 0 ? (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
No organizations found. You need to be part of at least one
|
No organizations found. You need to be part of at least one organization.
|
||||||
organization.
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<Select value={selectedOrg} onValueChange={setSelectedOrg}>
|
<Select value={selectedOrg} onValueChange={setSelectedOrg}>
|
||||||
<SelectTrigger
|
<SelectTrigger id="organization" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||||
id="organization"
|
|
||||||
className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Select organization" />
|
<SelectValue placeholder="Select organization" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -369,38 +295,24 @@ export function ConfigureStep() {
|
|||||||
|
|
||||||
{/* Deployment options */}
|
{/* Deployment options */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Label
|
<Label className={`text-sm mb-3 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
className={`text-sm mb-3 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
|
||||||
>
|
|
||||||
Deployment Type
|
Deployment Type
|
||||||
</Label>
|
</Label>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant={deployOption === 'lrn' ? 'default' : 'outline'}
|
variant={deployOption === 'lrn' ? "default" : "outline"}
|
||||||
className={`py-3 ${
|
className={`py-3 ${deployOption === 'lrn'
|
||||||
deployOption === 'lrn'
|
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
|
||||||
? isDarkMode
|
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
|
||||||
? 'bg-zinc-800 text-white'
|
|
||||||
: 'bg-zinc-800 text-white'
|
|
||||||
: isDarkMode
|
|
||||||
? 'bg-transparent border-zinc-700 text-zinc-400'
|
|
||||||
: 'bg-transparent border-zinc-300 text-zinc-600'
|
|
||||||
}`}
|
|
||||||
onClick={() => toggleDeployOption('lrn')}
|
onClick={() => toggleDeployOption('lrn')}
|
||||||
>
|
>
|
||||||
Deployer LRN
|
Deployer LRN
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={deployOption === 'auction' ? 'default' : 'outline'}
|
variant={deployOption === 'auction' ? "default" : "outline"}
|
||||||
className={`py-3 ${
|
className={`py-3 ${deployOption === 'auction'
|
||||||
deployOption === 'auction'
|
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
|
||||||
? isDarkMode
|
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
|
||||||
? 'bg-zinc-800 text-white'
|
|
||||||
: 'bg-zinc-800 text-white'
|
|
||||||
: isDarkMode
|
|
||||||
? 'bg-transparent border-zinc-700 text-zinc-400'
|
|
||||||
: 'bg-transparent border-zinc-300 text-zinc-600'
|
|
||||||
}`}
|
|
||||||
onClick={() => toggleDeployOption('auction')}
|
onClick={() => toggleDeployOption('auction')}
|
||||||
>
|
>
|
||||||
Create Auction
|
Create Auction
|
||||||
@ -411,18 +323,13 @@ export function ConfigureStep() {
|
|||||||
{deployOption === 'lrn' ? (
|
{deployOption === 'lrn' ? (
|
||||||
/* LRN Deployment Settings */
|
/* LRN Deployment Settings */
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Label
|
<Label htmlFor="lrn" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
htmlFor="lrn"
|
|
||||||
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
|
||||||
>
|
|
||||||
Select Deployer LRN *
|
Select Deployer LRN *
|
||||||
</Label>
|
</Label>
|
||||||
{isLoadingDeployers ? (
|
{isLoadingDeployers ? (
|
||||||
<div className="flex items-center justify-center p-3 border rounded-md">
|
<div className="flex items-center justify-center p-3 border rounded-md">
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">Loading deployers...</span>
|
||||||
Loading deployers...
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : deployers.length === 0 ? (
|
) : deployers.length === 0 ? (
|
||||||
<Alert>
|
<Alert>
|
||||||
@ -434,20 +341,12 @@ export function ConfigureStep() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Select value={selectedLrn} onValueChange={setSelectedLrn}>
|
<Select value={selectedLrn} onValueChange={setSelectedLrn}>
|
||||||
<SelectTrigger
|
<SelectTrigger id="lrn" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||||
id="lrn"
|
|
||||||
className={
|
|
||||||
isDarkMode ? 'border-zinc-700' : 'border-zinc-300'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Select a deployer" />
|
<SelectValue placeholder="Select a deployer" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{deployers.map((deployer) => (
|
{deployers.map((deployer) => (
|
||||||
<SelectItem
|
<SelectItem key={deployer.deployerLrn} value={deployer.deployerLrn}>
|
||||||
key={deployer.deployerLrn}
|
|
||||||
value={deployer.deployerLrn}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>{deployer.deployerLrn}</span>
|
<span>{deployer.deployerLrn}</span>
|
||||||
{deployer.minimumPayment && (
|
{deployer.minimumPayment && (
|
||||||
@ -465,19 +364,10 @@ export function ConfigureStep() {
|
|||||||
{selectedDeployer && (
|
{selectedDeployer && (
|
||||||
<div className="mt-3 p-3 bg-muted rounded-md">
|
<div className="mt-3 p-3 bg-muted rounded-md">
|
||||||
<div className="text-sm space-y-1">
|
<div className="text-sm space-y-1">
|
||||||
<div>
|
<div><strong>API URL:</strong> {selectedDeployer.deployerApiUrl}</div>
|
||||||
<strong>API URL:</strong>{' '}
|
<div><strong>Base Domain:</strong> {selectedDeployer.baseDomain}</div>
|
||||||
{selectedDeployer.deployerApiUrl}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Base Domain:</strong>{' '}
|
|
||||||
{selectedDeployer.baseDomain}
|
|
||||||
</div>
|
|
||||||
{selectedDeployer.minimumPayment && (
|
{selectedDeployer.minimumPayment && (
|
||||||
<div>
|
<div><strong>Minimum Payment:</strong> {selectedDeployer.minimumPayment}</div>
|
||||||
<strong>Minimum Payment:</strong>{' '}
|
|
||||||
{selectedDeployer.minimumPayment}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -489,20 +379,11 @@ export function ConfigureStep() {
|
|||||||
/* Auction Settings */
|
/* Auction Settings */
|
||||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<Label
|
<Label htmlFor="deployers" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
htmlFor="deployers"
|
|
||||||
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
|
||||||
>
|
|
||||||
Number of Deployers
|
Number of Deployers
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select value={numberOfDeployers} onValueChange={setNumberOfDeployers}>
|
||||||
value={numberOfDeployers}
|
<SelectTrigger id="deployers" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||||
onValueChange={setNumberOfDeployers}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="deployers"
|
|
||||||
className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Select number" />
|
<SelectValue placeholder="Select number" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -515,17 +396,11 @@ export function ConfigureStep() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label
|
<Label htmlFor="maxPrice" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
htmlFor="maxPrice"
|
|
||||||
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
|
||||||
>
|
|
||||||
Maximum Price (aint)
|
Maximum Price (aint)
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={maxPrice} onValueChange={setMaxPrice}>
|
<Select value={maxPrice} onValueChange={setMaxPrice}>
|
||||||
<SelectTrigger
|
<SelectTrigger id="maxPrice" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||||
id="maxPrice"
|
|
||||||
className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Select price" />
|
<SelectValue placeholder="Select price" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -541,14 +416,10 @@ export function ConfigureStep() {
|
|||||||
|
|
||||||
{/* Payment Address */}
|
{/* Payment Address */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Label
|
<Label className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
|
||||||
>
|
|
||||||
Payment Address
|
Payment Address
|
||||||
</Label>
|
</Label>
|
||||||
<div
|
<div className={`p-3 border rounded-md bg-muted ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}>
|
||||||
className={`p-3 border rounded-md bg-muted ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
|
||||||
>
|
|
||||||
<div className="text-sm font-mono break-all">
|
<div className="text-sm font-mono break-all">
|
||||||
{wallet?.address || 'No wallet connected'}
|
{wallet?.address || 'No wallet connected'}
|
||||||
</div>
|
</div>
|
||||||
@ -557,19 +428,12 @@ export function ConfigureStep() {
|
|||||||
|
|
||||||
{/* Environment Variables */}
|
{/* Environment Variables */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Label
|
<Label className={`text-sm font-medium mb-2 block ${isDarkMode ? 'text-white' : 'text-zinc-900'}`}>
|
||||||
className={`text-sm font-medium mb-2 block ${isDarkMode ? 'text-white' : 'text-zinc-900'}`}
|
|
||||||
>
|
|
||||||
Environment Variables
|
Environment Variables
|
||||||
</Label>
|
</Label>
|
||||||
<div
|
<div className={`border rounded-md p-4 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}>
|
||||||
className={`border rounded-md p-4 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
|
||||||
>
|
|
||||||
{envVars.map((envVar, index) => (
|
{envVars.map((envVar, index) => (
|
||||||
<div
|
<div key={index} className="space-y-2 mb-4 pb-4 border-b border-muted last:border-b-0 last:mb-0 last:pb-0">
|
||||||
key={index}
|
|
||||||
className="space-y-2 mb-4 pb-4 border-b border-muted last:border-b-0 last:mb-0 last:pb-0"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="KEY"
|
placeholder="KEY"
|
||||||
@ -580,16 +444,12 @@ export function ConfigureStep() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="VALUE"
|
placeholder="VALUE"
|
||||||
value={envVar.value}
|
value={envVar.value}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateEnvVar(index, 'value', e.target.value)}
|
||||||
updateEnvVar(index, 'value', e.target.value)
|
|
||||||
}
|
|
||||||
className={`bg-transparent ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
className={`bg-transparent ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">Environments:</span>
|
||||||
Environments:
|
|
||||||
</span>
|
|
||||||
{['Production', 'Preview', 'Development'].map((env) => (
|
{['Production', 'Preview', 'Development'].map((env) => (
|
||||||
<div key={env} className="flex items-center gap-1">
|
<div key={env} className="flex items-center gap-1">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@ -1,36 +1,22 @@
|
|||||||
// src/components/onboarding/connect-step/connect-step.tsx
|
// src/components/onboarding/connect-step/connect-step.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { GitHubBackendAuth } from '@/components/GitHubBackendAuth'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Github, Wallet, CheckCircle2, AlertTriangle, Loader2, ExternalLink, ChevronDown } from 'lucide-react'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
import { SignIn } from '@clerk/nextjs'
|
||||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||||
import { AVAILABLE_TEMPLATES, type TemplateDetail } from '@/constants/templates'
|
|
||||||
import { useAuthStatus } from '@/hooks/useAuthStatus'
|
import { useAuthStatus } from '@/hooks/useAuthStatus'
|
||||||
import { useRepoData } from '@/hooks/useRepoData'
|
import { useRepoData } from '@/hooks/useRepoData'
|
||||||
import type { Template } from '@/types/onboarding'
|
|
||||||
import { adaptOptionalTemplate } from '@/utils/typeAdapters'
|
|
||||||
import { SignIn } from '@clerk/nextjs'
|
|
||||||
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
|
||||||
import { Button } from '@workspace/ui/components/button'
|
import { Button } from '@workspace/ui/components/button'
|
||||||
import { Card, CardContent } from '@workspace/ui/components/card'
|
import { Card, CardContent } from '@workspace/ui/components/card'
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger
|
|
||||||
} from '@workspace/ui/components/collapsible'
|
|
||||||
import { Input } from '@workspace/ui/components/input'
|
import { Input } from '@workspace/ui/components/input'
|
||||||
import { Label } from '@workspace/ui/components/label'
|
import { Label } from '@workspace/ui/components/label'
|
||||||
import {
|
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||||
AlertTriangle,
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@workspace/ui/components/collapsible'
|
||||||
CheckCircle2,
|
|
||||||
ChevronDown,
|
|
||||||
ExternalLink,
|
|
||||||
Github,
|
|
||||||
Loader2,
|
|
||||||
Wallet
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { useTheme } from 'next-themes'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { GitHubBackendAuth } from '@/components/GitHubBackendAuth'
|
||||||
|
import { AVAILABLE_TEMPLATES, type TemplateDetail } from '@/constants/templates'
|
||||||
|
|
||||||
interface Repository {
|
interface Repository {
|
||||||
id: string | number
|
id: string | number
|
||||||
@ -45,15 +31,11 @@ export function ConnectStep() {
|
|||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
// Repository vs Template selection
|
// Repository vs Template selection
|
||||||
const [selectedRepo, setSelectedRepo] = useState<string>(
|
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
|
||||||
formData.githubRepo || ''
|
const [selectedTemplate, setSelectedTemplate] = useState<TemplateDetail | undefined>(
|
||||||
)
|
formData.template || undefined
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState(
|
|
||||||
adaptOptionalTemplate(formData.template)
|
|
||||||
)
|
|
||||||
const [projectName, setProjectName] = useState<string>(
|
|
||||||
formData.projectName || ''
|
|
||||||
)
|
)
|
||||||
|
const [projectName, setProjectName] = useState<string>(formData.projectName || '')
|
||||||
const [isImportMode, setIsImportMode] = useState(true)
|
const [isImportMode, setIsImportMode] = useState(true)
|
||||||
|
|
||||||
// Auth status and warning display
|
// Auth status and warning display
|
||||||
@ -62,6 +44,8 @@ export function ConnectStep() {
|
|||||||
// Auth status hook
|
// Auth status hook
|
||||||
const {
|
const {
|
||||||
clerk,
|
clerk,
|
||||||
|
wallet,
|
||||||
|
backend,
|
||||||
isFullyAuthenticated,
|
isFullyAuthenticated,
|
||||||
isReady,
|
isReady,
|
||||||
missing,
|
missing,
|
||||||
@ -110,9 +94,7 @@ export function ConnectStep() {
|
|||||||
template: template,
|
template: template,
|
||||||
githubRepo: '',
|
githubRepo: '',
|
||||||
deploymentMode: 'template',
|
deploymentMode: 'template',
|
||||||
projectName:
|
projectName: projectName || `my-${template.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
||||||
projectName ||
|
|
||||||
`my-${template.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,14 +163,13 @@ export function ConnectStep() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For repository import, project name is optional but we'll use repo name as fallback
|
// For repository import, project name is optional but we'll use repo name as fallback
|
||||||
const finalProjectName =
|
const finalProjectName = projectName.trim() || (isImportMode ? selectedRepo.split('/')[1] : '')
|
||||||
projectName.trim() || (isImportMode ? selectedRepo.split('/')[1] : '')
|
|
||||||
|
|
||||||
// Set final form data and proceed
|
// Set final form data and proceed
|
||||||
setFormData({
|
setFormData({
|
||||||
deploymentMode: isImportMode ? 'repository' : 'template',
|
deploymentMode: isImportMode ? 'repository' : 'template',
|
||||||
githubRepo: isImportMode ? selectedRepo : '',
|
githubRepo: isImportMode ? selectedRepo : '',
|
||||||
template: !isImportMode ? (selectedTemplate as Template) : undefined,
|
template: !isImportMode ? selectedTemplate : undefined,
|
||||||
projectName: finalProjectName
|
projectName: finalProjectName
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -201,9 +182,7 @@ export function ConnectStep() {
|
|||||||
<div className="w-full h-full flex items-center justify-center">
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
|
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
|
||||||
<p className="text-sm text-zinc-500">
|
<p className="text-sm text-zinc-500">Loading authentication status...</p>
|
||||||
Loading authentication status...
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -217,9 +196,7 @@ export function ConnectStep() {
|
|||||||
<div className="max-w-2xl w-full mx-auto">
|
<div className="max-w-2xl w-full mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h2
|
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-2`}>
|
||||||
className={`text-2xl font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} mb-2`}
|
|
||||||
>
|
|
||||||
Connect
|
Connect
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-zinc-500 mb-6">
|
<p className="text-zinc-500 mb-6">
|
||||||
@ -227,15 +204,12 @@ export function ConnectStep() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* GitHub Account Selector - Only show if multiple accounts */}
|
{/* GitHub Account Selector - Only show if multiple accounts */}
|
||||||
{clerk.user?.externalAccounts &&
|
{clerk.user?.externalAccounts && clerk.user.externalAccounts.length > 1 && (
|
||||||
clerk.user.externalAccounts.length > 1 && (
|
|
||||||
<div className="flex items-center justify-center mb-6">
|
<div className="flex items-center justify-center mb-6">
|
||||||
<div className="flex items-center gap-2 px-4 py-2 bg-zinc-100 dark:bg-zinc-800 rounded-md cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-700">
|
<div className="flex items-center gap-2 px-4 py-2 bg-zinc-100 dark:bg-zinc-800 rounded-md cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-700">
|
||||||
<Github className="h-4 w-4" />
|
<Github className="h-4 w-4" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{clerk.user?.externalAccounts?.find(
|
{clerk.user?.externalAccounts?.find(acc => acc.provider === 'github')?.username || 'git-account'}
|
||||||
(acc: any) => acc.provider === 'github'
|
|
||||||
)?.username || 'git-account'}
|
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
@ -250,10 +224,7 @@ export function ConnectStep() {
|
|||||||
<Alert className="mb-6 cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/20">
|
<Alert className="mb-6 cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/20">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<AlertDescription className="flex items-center justify-between w-full">
|
<AlertDescription className="flex items-center justify-between w-full">
|
||||||
<span>
|
<span>Authentication required to continue ({progress.completed}/{progress.total} complete)</span>
|
||||||
Authentication required to continue ({progress.completed}/
|
|
||||||
{progress.total} complete)
|
|
||||||
</span>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
@ -265,9 +236,7 @@ export function ConnectStep() {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<Github className="h-4 w-4" />
|
<Github className="h-4 w-4" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">Sign in with Clerk</span>
|
||||||
Sign in with Clerk
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="scale-90 origin-top-left">
|
<div className="scale-90 origin-top-left">
|
||||||
<SignIn routing="hash" />
|
<SignIn routing="hash" />
|
||||||
@ -281,15 +250,9 @@ export function ConnectStep() {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<Github className="h-4 w-4" />
|
<Github className="h-4 w-4" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">Connect GitHub Account</span>
|
||||||
Connect GitHub Account
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button size="sm" variant="outline" onClick={() => window.open('/user-profile', '_blank')}>
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => window.open('/user-profile', '_blank')}
|
|
||||||
>
|
|
||||||
<ExternalLink className="h-3 w-3 mr-2" />
|
<ExternalLink className="h-3 w-3 mr-2" />
|
||||||
Connect GitHub
|
Connect GitHub
|
||||||
</Button>
|
</Button>
|
||||||
@ -302,9 +265,7 @@ export function ConnectStep() {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<Wallet className="h-4 w-4" />
|
<Wallet className="h-4 w-4" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">Connect Wallet</span>
|
||||||
Connect Wallet
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" onClick={handleConnectWallet}>
|
<Button size="sm" onClick={handleConnectWallet}>
|
||||||
Connect Wallet
|
Connect Wallet
|
||||||
@ -313,20 +274,14 @@ export function ConnectStep() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{missing.githubBackendSync &&
|
{missing.githubBackendSync && !missing.walletConnection && !missing.clerkGithub && (
|
||||||
!missing.walletConnection &&
|
|
||||||
!missing.clerkGithub && (
|
|
||||||
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<Github className="h-4 w-4" />
|
<Github className="h-4 w-4" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">Sync GitHub Access</span>
|
||||||
Sync GitHub Access
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<GitHubBackendAuth
|
<GitHubBackendAuth onAuthStatusChange={handleGithubAuthChange} />
|
||||||
onAuthStatusChange={handleGithubAuthChange}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
@ -337,9 +292,8 @@ export function ConnectStep() {
|
|||||||
{/* Mode Selection Tabs */}
|
{/* Mode Selection Tabs */}
|
||||||
<div className="grid grid-cols-2 gap-1 p-1 bg-zinc-100 dark:bg-zinc-800 rounded-lg mb-6">
|
<div className="grid grid-cols-2 gap-1 p-1 bg-zinc-100 dark:bg-zinc-800 rounded-lg mb-6">
|
||||||
<Button
|
<Button
|
||||||
variant={isImportMode ? 'default' : 'ghost'}
|
variant={isImportMode ? "default" : "ghost"}
|
||||||
className={`${
|
className={`${isImportMode
|
||||||
isImportMode
|
|
||||||
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
||||||
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
||||||
}`}
|
}`}
|
||||||
@ -348,9 +302,8 @@ export function ConnectStep() {
|
|||||||
Import a repository
|
Import a repository
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={!isImportMode ? 'default' : 'ghost'}
|
variant={!isImportMode ? "default" : "ghost"}
|
||||||
className={`${
|
className={`${!isImportMode
|
||||||
!isImportMode
|
|
||||||
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
||||||
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
||||||
}`}
|
}`}
|
||||||
@ -374,8 +327,7 @@ export function ConnectStep() {
|
|||||||
<Alert>
|
<Alert>
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
No repositories found. Make sure your GitHub account has
|
No repositories found. Make sure your GitHub account has repositories.
|
||||||
repositories.
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
@ -394,13 +346,9 @@ export function ConnectStep() {
|
|||||||
>
|
>
|
||||||
<Github className="h-5 w-5 mr-3 text-zinc-500 flex-shrink-0" />
|
<Github className="h-5 w-5 mr-3 text-zinc-500 flex-shrink-0" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-medium text-sm">
|
<div className="font-medium text-sm">{repo.full_name}</div>
|
||||||
{repo.full_name}
|
|
||||||
</div>
|
|
||||||
{repo.description && (
|
{repo.description && (
|
||||||
<div className="text-xs text-zinc-500 truncate">
|
<div className="text-xs text-zinc-500 truncate">{repo.description}</div>
|
||||||
{repo.description}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{selectedRepo === repo.full_name && (
|
{selectedRepo === repo.full_name && (
|
||||||
@ -413,10 +361,7 @@ export function ConnectStep() {
|
|||||||
{/* Project Name Input for Repository Import */}
|
{/* Project Name Input for Repository Import */}
|
||||||
{selectedRepo && (
|
{selectedRepo && (
|
||||||
<div className="mt-6 space-y-2">
|
<div className="mt-6 space-y-2">
|
||||||
<Label
|
<Label htmlFor="projectName" className="text-sm font-medium">
|
||||||
htmlFor="projectName"
|
|
||||||
className="text-sm font-medium"
|
|
||||||
>
|
|
||||||
Project Name
|
Project Name
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -437,8 +382,7 @@ export function ConnectStep() {
|
|||||||
) : (
|
) : (
|
||||||
/* Template Selection */
|
/* Template Selection */
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{AVAILABLE_TEMPLATES.filter((t) => !t.isComingSoon).map(
|
{AVAILABLE_TEMPLATES.filter(t => !t.isComingSoon).map((template) => (
|
||||||
(template) => (
|
|
||||||
<div
|
<div
|
||||||
key={template.id}
|
key={template.id}
|
||||||
className={`flex items-center p-4 rounded-lg border cursor-pointer transition-all ${
|
className={`flex items-center p-4 rounded-lg border cursor-pointer transition-all ${
|
||||||
@ -451,19 +395,13 @@ export function ConnectStep() {
|
|||||||
{/* Template Icon */}
|
{/* Template Icon */}
|
||||||
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-zinc-100 dark:bg-zinc-800 mr-4">
|
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-zinc-100 dark:bg-zinc-800 mr-4">
|
||||||
<div className="w-6 h-6 bg-zinc-600 dark:bg-zinc-400 rounded flex items-center justify-center text-xs font-bold text-white">
|
<div className="w-6 h-6 bg-zinc-600 dark:bg-zinc-400 rounded flex items-center justify-center text-xs font-bold text-white">
|
||||||
{template.icon === 'web'
|
{template.icon === 'web' ? 'PWA' : template.icon === 'nextjs' ? 'N' : 'IMG'}
|
||||||
? 'PWA'
|
|
||||||
: template.icon === 'nextjs'
|
|
||||||
? 'N'
|
|
||||||
: 'IMG'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Template Info */}
|
{/* Template Info */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-medium text-sm mb-1">
|
<div className="font-medium text-sm mb-1">{template.name}</div>
|
||||||
{template.name}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-xs text-zinc-500">
|
<div className="flex items-center text-xs text-zinc-500">
|
||||||
<Github className="h-3 w-3 mr-1" />
|
<Github className="h-3 w-3 mr-1" />
|
||||||
{template.repoFullName}
|
{template.repoFullName}
|
||||||
@ -475,8 +413,7 @@ export function ConnectStep() {
|
|||||||
<CheckCircle2 className="h-5 w-5 text-blue-500 flex-shrink-0" />
|
<CheckCircle2 className="h-5 w-5 text-blue-500 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Project Name Input for Templates */}
|
{/* Project Name Input for Templates */}
|
||||||
{selectedTemplate && (
|
{selectedTemplate && (
|
||||||
@ -506,12 +443,7 @@ export function ConnectStep() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
disabled={
|
disabled={!isFullyAuthenticated || (isImportMode ? !selectedRepo : (!selectedTemplate || !projectName.trim()))}
|
||||||
!isFullyAuthenticated ||
|
|
||||||
(isImportMode
|
|
||||||
? !selectedRepo
|
|
||||||
: !selectedTemplate || !projectName.trim())
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -90,7 +90,6 @@ export const ConnectStep = () => {
|
|||||||
<StepHeader
|
<StepHeader
|
||||||
title="Connect"
|
title="Connect"
|
||||||
description="Connect and import a GitHub repo or start from a template"
|
description="Connect and import a GitHub repo or start from a template"
|
||||||
icon={<GitBranch />}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Git account selector */}
|
{/* Git account selector */}
|
||||||
|
|||||||
@ -1,30 +1,20 @@
|
|||||||
// src/components/onboarding/deploy-step/deploy-step.tsx
|
// src/components/onboarding/deploy-step/deploy-step.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
import { Github, Loader2, AlertTriangle, CheckCircle2 } from 'lucide-react'
|
||||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||||
|
import { useGQLClient } from '@/context'
|
||||||
import { useWallet } from '@/context/WalletContext'
|
import { useWallet } from '@/context/WalletContext'
|
||||||
import { useDeployment } from '@/hooks/useDeployment'
|
import { useDeployment } from '@/hooks/useDeployment'
|
||||||
import { useTemplateDeployment } from '@/hooks/useTemplate'
|
import { useTemplateDeployment } from '@/hooks/useTemplate'
|
||||||
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
|
||||||
import { Badge } from '@workspace/ui/components/badge'
|
|
||||||
import { Button } from '@workspace/ui/components/button'
|
import { Button } from '@workspace/ui/components/button'
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle
|
|
||||||
} from '@workspace/ui/components/card'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle
|
|
||||||
} from '@workspace/ui/components/dialog'
|
|
||||||
import { Progress } from '@workspace/ui/components/progress'
|
import { Progress } from '@workspace/ui/components/progress'
|
||||||
import { AlertTriangle, CheckCircle2, Github, Loader2 } from 'lucide-react'
|
import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogFooter } from '@workspace/ui/components/dialog'
|
||||||
import { useTheme } from 'next-themes'
|
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||||
import { useEffect, useState } from 'react'
|
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||||
|
import { Badge } from '@workspace/ui/components/badge'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
export function DeployStep() {
|
export function DeployStep() {
|
||||||
@ -40,8 +30,7 @@ export function DeployStep() {
|
|||||||
// Contexts and hooks
|
// Contexts and hooks
|
||||||
const { wallet } = useWallet()
|
const { wallet } = useWallet()
|
||||||
const { deployRepository, isDeploying: isRepoDeploying } = useDeployment()
|
const { deployRepository, isDeploying: isRepoDeploying } = useDeployment()
|
||||||
const { deployTemplate, isDeploying: isTemplateDeploying } =
|
const { deployTemplate, isDeploying: isTemplateDeploying } = useTemplateDeployment()
|
||||||
useTemplateDeployment()
|
|
||||||
|
|
||||||
// Determine deployment type and get the right deploying state
|
// Determine deployment type and get the right deploying state
|
||||||
const isTemplateMode = formData.deploymentMode === 'template'
|
const isTemplateMode = formData.deploymentMode === 'template'
|
||||||
@ -65,10 +54,7 @@ export function DeployStep() {
|
|||||||
return {
|
return {
|
||||||
name: formData.githubRepo?.split('/').pop() || 'Repository',
|
name: formData.githubRepo?.split('/').pop() || 'Repository',
|
||||||
source: formData.githubRepo || 'Unknown Repository',
|
source: formData.githubRepo || 'Unknown Repository',
|
||||||
projectName:
|
projectName: formData.projectName || formData.githubRepo?.split('/').pop() || 'New Project',
|
||||||
formData.projectName ||
|
|
||||||
formData.githubRepo?.split('/').pop() ||
|
|
||||||
'New Project',
|
|
||||||
type: 'Repository'
|
type: 'Repository'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,9 +106,7 @@ export function DeployStep() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Deployment failed:', error)
|
console.error('Deployment failed:', error)
|
||||||
setDeploymentError(
|
setDeploymentError(error instanceof Error ? error.message : 'Deployment failed')
|
||||||
error instanceof Error ? error.message : 'Deployment failed'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,16 +117,15 @@ export function DeployStep() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
template: {
|
template: formData.template,
|
||||||
...formData.template,
|
|
||||||
id: formData.template?.id || '',
|
|
||||||
icon: formData.template?.icon || ''
|
|
||||||
},
|
|
||||||
projectName: formData.projectName,
|
projectName: formData.projectName,
|
||||||
organizationSlug: formData.selectedOrg,
|
organizationSlug: formData.selectedOrg,
|
||||||
environmentVariables: formData.environmentVariables || [],
|
environmentVariables: formData.environmentVariables || [],
|
||||||
deployerLrn: formData.selectedLrn
|
deployerLrn: formData.selectedLrn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Deploying template with config:', config)
|
||||||
|
|
||||||
const result = await deployTemplate(config)
|
const result = await deployTemplate(config)
|
||||||
|
|
||||||
// Save deployment results
|
// Save deployment results
|
||||||
@ -173,10 +156,7 @@ export function DeployStep() {
|
|||||||
organizationSlug: formData.selectedOrg,
|
organizationSlug: formData.selectedOrg,
|
||||||
repository: formData.githubRepo,
|
repository: formData.githubRepo,
|
||||||
branch: 'main', // Default branch
|
branch: 'main', // Default branch
|
||||||
name:
|
name: formData.projectName || formData.githubRepo.split('/').pop() || 'New Project',
|
||||||
formData.projectName ||
|
|
||||||
formData.githubRepo.split('/').pop() ||
|
|
||||||
'New Project',
|
|
||||||
environmentVariables: formData.environmentVariables || []
|
environmentVariables: formData.environmentVariables || []
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,66 +194,18 @@ export function DeployStep() {
|
|||||||
<div className="max-w-md w-full mx-auto">
|
<div className="max-w-md w-full mx-auto">
|
||||||
{/* Deploy icon */}
|
{/* Deploy icon */}
|
||||||
<div className="mx-auto mb-6 flex justify-center">
|
<div className="mx-auto mb-6 flex justify-center">
|
||||||
<svg
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={isDarkMode ? "text-white" : "text-black"}>
|
||||||
width="48"
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
height="48"
|
<polyline points="7.5 4.21 12 6.81 16.5 4.21" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
viewBox="0 0 24 24"
|
<polyline points="7.5 19.79 7.5 14.6 3 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
fill="none"
|
<polyline points="21 12 16.5 14.6 16.5 19.79" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
className={isDarkMode ? 'text-white' : 'text-black'}
|
<line x1="12" y1="22.08" x2="12" y2="12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
points="7.5 4.21 12 6.81 16.5 4.21"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
points="7.5 19.79 7.5 14.6 3 12"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
points="21 12 16.5 14.6 16.5 19.79"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
points="3.27 6.96 12 12.01 20.73 6.96"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="12"
|
|
||||||
y1="22.08"
|
|
||||||
x2="12"
|
|
||||||
y2="12"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Deploy header */}
|
{/* Deploy header */}
|
||||||
<h2
|
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>
|
||||||
className={`text-2xl font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} text-center mb-2`}
|
|
||||||
>
|
|
||||||
{isDeploying ? 'Deploying...' : 'Deploy'}
|
{isDeploying ? 'Deploying...' : 'Deploy'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-center text-zinc-500 mb-8">
|
<p className="text-center text-zinc-500 mb-8">
|
||||||
@ -294,9 +226,7 @@ export function DeployStep() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Github className="h-4 w-4 text-muted-foreground" />
|
<Github className="h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-medium text-sm">
|
<div className="font-medium text-sm">{deploymentInfo.projectName}</div>
|
||||||
{deploymentInfo.projectName}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground font-mono">
|
<div className="text-xs text-muted-foreground font-mono">
|
||||||
{deploymentInfo.source}
|
{deploymentInfo.source}
|
||||||
</div>
|
</div>
|
||||||
@ -310,22 +240,15 @@ export function DeployStep() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground">Deployer</div>
|
<div className="text-muted-foreground">Deployer</div>
|
||||||
<div className="font-medium">
|
<div className="font-medium">{formData.selectedLrn ? 'LRN' : 'Auction'}</div>
|
||||||
{formData.selectedLrn ? 'LRN' : 'Auction'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formData.environmentVariables &&
|
{formData.environmentVariables && formData.environmentVariables.length > 0 && (
|
||||||
formData.environmentVariables.length > 0 && (
|
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<div className="text-xs text-muted-foreground mb-1">
|
<div className="text-xs text-muted-foreground mb-1">Environment Variables</div>
|
||||||
Environment Variables
|
|
||||||
</div>
|
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
{formData.environmentVariables.length} variable
|
{formData.environmentVariables.length} variable{formData.environmentVariables.length !== 1 ? 's' : ''} configured
|
||||||
{formData.environmentVariables.length !== 1 ? 's' : ''}{' '}
|
|
||||||
configured
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -350,8 +273,7 @@ export function DeployStep() {
|
|||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<div className="font-medium">Deployment Successful!</div>
|
<div className="font-medium">Deployment Successful!</div>
|
||||||
<div className="text-sm mt-1">
|
<div className="text-sm mt-1">
|
||||||
Your project has been deployed successfully. You'll be
|
Your project has been deployed successfully. You'll be redirected to the project dashboard.
|
||||||
redirected to the project dashboard.
|
|
||||||
</div>
|
</div>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
@ -361,21 +283,13 @@ export function DeployStep() {
|
|||||||
{isDeploying && (
|
{isDeploying && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<div
|
<div className={`${isDarkMode ? "text-white" : "text-zinc-900"} text-sm`}>
|
||||||
className={`${isDarkMode ? 'text-white' : 'text-zinc-900'} text-sm`}
|
{isTemplateMode ? 'Creating repository from template...' : 'Deploying repository...'}
|
||||||
>
|
|
||||||
{isTemplateMode
|
|
||||||
? 'Creating repository from template...'
|
|
||||||
: 'Deploying repository...'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress value={undefined} className={`h-2 ${isDarkMode ? "bg-zinc-800" : "bg-zinc-200"}`} />
|
||||||
value={undefined}
|
|
||||||
className={`h-2 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'}`}
|
|
||||||
/>
|
|
||||||
<div className="text-xs text-muted-foreground mt-2">
|
<div className="text-xs text-muted-foreground mt-2">
|
||||||
This process may take several minutes. Please do not close this
|
This process may take several minutes. Please do not close this window.
|
||||||
window.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -384,7 +298,7 @@ export function DeployStep() {
|
|||||||
<div className="flex justify-between items-center mt-4">
|
<div className="flex justify-between items-center mt-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`${isDarkMode ? 'text-zinc-400 border-zinc-700' : 'text-zinc-600 border-zinc-300'} bg-transparent`}
|
className={`${isDarkMode ? "text-zinc-400 border-zinc-700" : "text-zinc-600 border-zinc-300"} bg-transparent`}
|
||||||
onClick={previousStep}
|
onClick={previousStep}
|
||||||
disabled={isDeploying || deploymentSuccess}
|
disabled={isDeploying || deploymentSuccess}
|
||||||
>
|
>
|
||||||
@ -401,7 +315,7 @@ export function DeployStep() {
|
|||||||
</Button>
|
</Button>
|
||||||
) : isDeploying ? (
|
) : isDeploying ? (
|
||||||
<Button
|
<Button
|
||||||
className={`${isDarkMode ? 'bg-zinc-700 text-zinc-300' : 'bg-zinc-300 text-zinc-600'}`}
|
className={`${isDarkMode ? "bg-zinc-700 text-zinc-300" : "bg-zinc-300 text-zinc-600"}`}
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
@ -413,22 +327,9 @@ export function DeployStep() {
|
|||||||
onClick={handlePayAndDeploy}
|
onClick={handlePayAndDeploy}
|
||||||
disabled={deploymentError !== null}
|
disabled={deploymentError !== null}
|
||||||
>
|
>
|
||||||
{formData.deploymentType === 'auction'
|
{formData.deploymentType === 'auction' ? 'Pay and Deploy' : 'Deploy'}
|
||||||
? 'Pay and Deploy'
|
<svg className="ml-2 h-4 w-4" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
: 'Deploy'}
|
<path d="M5 12H19M19 12L13 6M19 12L13 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
<svg
|
|
||||||
className="ml-2 h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M5 12H19M19 12L13 6M19 12L13 18"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@ -459,9 +360,7 @@ export function DeployStep() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">Source:</span>
|
<span className="text-muted-foreground">Source:</span>
|
||||||
<span className="font-mono text-xs">
|
<span className="font-mono text-xs">{deploymentInfo.source}</span>
|
||||||
{deploymentInfo.source}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -479,9 +378,7 @@ export function DeployStep() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-sm font-medium">Deployer</h3>
|
<h3 className="text-sm font-medium">Deployer</h3>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<div className="font-mono text-xs">
|
<div className="font-mono text-xs">{formData.selectedLrn}</div>
|
||||||
{formData.selectedLrn}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -505,10 +402,17 @@ export function DeployStep() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex justify-end space-x-2">
|
<DialogFooter className="flex justify-end space-x-2">
|
||||||
<Button variant="outline" onClick={handleCancelConfirm}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancelConfirm}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirmDeploy}>Confirm Deployment</Button>
|
<Button
|
||||||
|
onClick={handleConfirmDeploy}
|
||||||
|
>
|
||||||
|
Confirm Deployment
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -10,7 +10,10 @@
|
|||||||
|
|
||||||
// Main component
|
// Main component
|
||||||
export { default as Onboarding } from './Onboarding'
|
export { default as Onboarding } from './Onboarding'
|
||||||
export { default as OnboardingDialog } from './OnboardingDialog'
|
export {
|
||||||
|
default as OnboardingDialog,
|
||||||
|
hasCompletedOnboarding
|
||||||
|
} from './OnboardingDialog'
|
||||||
|
|
||||||
// Step components
|
// Step components
|
||||||
export { ConfigureStep } from './configure-step'
|
export { ConfigureStep } from './configure-step'
|
||||||
@ -25,7 +28,7 @@ export * from './common'
|
|||||||
export * from './sidebar'
|
export * from './sidebar'
|
||||||
|
|
||||||
// Store and hooks
|
// Store and hooks
|
||||||
export { useOnboarding } from './useOnboarding'
|
export { useOnboarding } from './store'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
/**
|
/**
|
||||||
* Available steps in the onboarding flow
|
* Available steps in the onboarding flow
|
||||||
*/
|
*/
|
||||||
export type Step = 'connect' | 'configure' | 'deploy' | 'success'
|
export type Step = 'connect' | 'configure' | 'deploy'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form data collected during the onboarding process
|
* Form data collected during the onboarding process
|
||||||
|
|||||||
@ -1,120 +1,86 @@
|
|||||||
// src/components/onboarding/useOnboarding.ts
|
'use client'
|
||||||
import { useState } from 'react'
|
|
||||||
import type {
|
import { create } from 'zustand'
|
||||||
OnboardingFormData} from '@/types'
|
import { persist } from 'zustand/middleware'
|
||||||
|
|
||||||
// Step type for navigation
|
|
||||||
export type Step = 'connect' | 'configure' | 'deploy' | 'success'
|
export type Step = 'connect' | 'configure' | 'deploy' | 'success'
|
||||||
|
|
||||||
// Initial form data
|
export interface EnvironmentVariables {
|
||||||
const initialFormData: OnboardingFormData = {
|
key: string
|
||||||
// Step 1: Connect
|
value: string
|
||||||
githubRepo: undefined,
|
|
||||||
template: undefined,
|
|
||||||
projectName: undefined,
|
|
||||||
deploymentMode: undefined,
|
|
||||||
|
|
||||||
// Step 2: Configure
|
|
||||||
selectedLrn: undefined,
|
|
||||||
selectedOrg: undefined, // Add this property
|
|
||||||
environmentVariables: [], // Use array type
|
|
||||||
deploymentType: undefined, // Add missing properties
|
|
||||||
deployerCount: undefined,
|
|
||||||
maxPrice: undefined,
|
|
||||||
|
|
||||||
// Step 3: Deploy
|
|
||||||
deploymentId: undefined,
|
|
||||||
repositoryUrl: undefined,
|
|
||||||
deploymentUrl: undefined, // Add missing property
|
|
||||||
projectId: undefined // Add missing property
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOnboarding() {
|
export interface OnboardingFormData {
|
||||||
const [formData, setFormData] = useState<OnboardingFormData>(initialFormData)
|
// Connect step
|
||||||
const [currentStep, setCurrentStep] = useState<Step>('connect')
|
githubRepo?: string
|
||||||
|
|
||||||
// Update form data (partial update)
|
// Configure step
|
||||||
const updateFormData = (updates: Partial<OnboardingFormData>) => {
|
deploymentType?: 'auction' | 'lrn'
|
||||||
setFormData(prev => ({ ...prev, ...updates }))
|
deployerCount?: string
|
||||||
|
maxPrice?: string
|
||||||
|
selectedLrn?: string
|
||||||
|
environments?: {
|
||||||
|
production: boolean
|
||||||
|
preview: boolean
|
||||||
|
development: boolean
|
||||||
}
|
}
|
||||||
|
environmentVariables?: Record<string, string>
|
||||||
|
|
||||||
// Reset form data
|
// Deploy step
|
||||||
const resetFormData = () => {
|
deploymentId?: string
|
||||||
setFormData(initialFormData)
|
deploymentUrl?: string
|
||||||
setCurrentStep('connect')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step navigation
|
// Success step
|
||||||
const nextStep = () => {
|
projectId?: string
|
||||||
switch (currentStep) {
|
}
|
||||||
case 'connect':
|
|
||||||
setCurrentStep('configure')
|
|
||||||
break
|
|
||||||
case 'configure':
|
|
||||||
setCurrentStep('deploy')
|
|
||||||
break
|
|
||||||
case 'deploy':
|
|
||||||
setCurrentStep('success')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousStep = () => {
|
interface OnboardingState {
|
||||||
switch (currentStep) {
|
currentStep: Step
|
||||||
case 'configure':
|
formData: OnboardingFormData
|
||||||
setCurrentStep('connect')
|
setCurrentStep: (step: Step) => void
|
||||||
break
|
setFormData: (data: Partial<OnboardingFormData>) => void
|
||||||
case 'deploy':
|
nextStep: () => void
|
||||||
setCurrentStep('configure')
|
previousStep: () => void
|
||||||
break
|
resetOnboarding: () => void
|
||||||
case 'success':
|
}
|
||||||
setCurrentStep('deploy')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation helpers
|
const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy', 'success']
|
||||||
const validateStep1 = () => {
|
|
||||||
if (formData.deploymentMode === 'template') {
|
|
||||||
return !!(formData.template && formData.projectName)
|
|
||||||
}
|
|
||||||
return !!formData.githubRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateStep2 = () => {
|
export const useOnboarding = create<OnboardingState>()(
|
||||||
return !!(formData.selectedLrn && formData.selectedOrg)
|
persist(
|
||||||
}
|
(set) => ({
|
||||||
|
currentStep: 'connect',
|
||||||
|
formData: {},
|
||||||
|
|
||||||
const validateStep3 = () => {
|
setCurrentStep: (step) => set({ currentStep: step }),
|
||||||
return !!formData.deploymentId
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
setFormData: (data) =>
|
||||||
// Data
|
set((state) => ({
|
||||||
formData,
|
formData: { ...state.formData, ...data }
|
||||||
currentStep,
|
})),
|
||||||
|
|
||||||
// Actions
|
nextStep: () =>
|
||||||
setFormData: updateFormData,
|
set((state) => {
|
||||||
setCurrentStep,
|
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
|
||||||
resetFormData,
|
const nextStep = STEP_ORDER[currentIndex + 1]
|
||||||
resetOnboarding: resetFormData, // Alias for compatibility
|
return nextStep ? { currentStep: nextStep } : state
|
||||||
nextStep,
|
}),
|
||||||
previousStep,
|
|
||||||
|
|
||||||
// Validation
|
previousStep: () =>
|
||||||
validateStep1,
|
set((state) => {
|
||||||
validateStep2,
|
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
|
||||||
validateStep3,
|
const previousStep = STEP_ORDER[currentIndex - 1]
|
||||||
|
return previousStep ? { currentStep: previousStep } : state
|
||||||
|
}),
|
||||||
|
|
||||||
// Store-like interface for compatibility
|
resetOnboarding: () =>
|
||||||
getState: () => ({
|
set({
|
||||||
formData,
|
currentStep: 'connect',
|
||||||
currentStep,
|
formData: {}
|
||||||
nextStep,
|
|
||||||
previousStep,
|
|
||||||
setFormData: updateFormData,
|
|
||||||
resetFormData
|
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'laconic-onboarding-storage'
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
)
|
||||||
@ -19,7 +19,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
const initGQLClient = async () => {
|
const initGQLClient = async () => {
|
||||||
try {
|
try {
|
||||||
const client = new GQLClient({
|
const client = new GQLClient({
|
||||||
gqlEndpoint: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'}/graphql`,
|
gqlEndpoint: 'http://localhost:8000/graphql',
|
||||||
})
|
})
|
||||||
setGqlClient(client)
|
setGqlClient(client)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -8,9 +8,9 @@
|
|||||||
export interface TemplateDetail {
|
export interface TemplateDetail {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
icon: string // Required string
|
icon: string
|
||||||
repoFullName: string
|
repoFullName: string
|
||||||
description: string // Required string
|
description: string
|
||||||
isComingSoon?: boolean
|
isComingSoon?: boolean
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,6 @@ import {
|
|||||||
useCallback
|
useCallback
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
|
||||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @interface BackendContextType
|
* @interface BackendContextType
|
||||||
* @description Defines the structure of the BackendContext value.
|
* @description Defines the structure of the BackendContext value.
|
||||||
@ -45,9 +43,9 @@ export const BackendProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
// Check backend connection
|
// Check backend connection
|
||||||
const checkBackendConnection = useCallback(async (): Promise<void> => {
|
const checkBackendConnection = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
@ -61,11 +59,11 @@ export const BackendProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
console.log('❌ Backend not connected')
|
console.log('❌ Backend not connected')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't return anything - function returns Promise<void>
|
return connected
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking backend connection:', error)
|
console.error('Error checking backend connection:', error)
|
||||||
setIsBackendConnected(false)
|
setIsBackendConnected(false)
|
||||||
// Don't return anything - function returns Promise<void>
|
return false
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@ -143,7 +143,7 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
|||||||
// Calculate what's missing
|
// Calculate what's missing
|
||||||
const missing = {
|
const missing = {
|
||||||
clerkSignIn: !isSignedIn,
|
clerkSignIn: !isSignedIn,
|
||||||
clerkGithub: (isSignedIn ?? false) && !hasGithubInClerk,
|
clerkGithub: isSignedIn && !hasGithubInClerk,
|
||||||
walletConnection: !hasWalletAddress, // Just need wallet address for this step
|
walletConnection: !hasWalletAddress, // Just need wallet address for this step
|
||||||
backendConnection: hasWalletAddress && !isWalletSessionActive, // Need SIWE auth for backend
|
backendConnection: hasWalletAddress && !isWalletSessionActive, // Need SIWE auth for backend
|
||||||
githubBackendSync: isBackendConnected && !isGithubBackendAuth
|
githubBackendSync: isBackendConnected && !isGithubBackendAuth
|
||||||
@ -177,7 +177,7 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
|||||||
return {
|
return {
|
||||||
// Individual systems
|
// Individual systems
|
||||||
clerk: {
|
clerk: {
|
||||||
isSignedIn: isSignedIn ?? false,
|
isSignedIn,
|
||||||
isLoaded: isClerkLoaded && isUserLoaded,
|
isLoaded: isClerkLoaded && isUserLoaded,
|
||||||
hasGithubConnected: hasGithubInClerk,
|
hasGithubConnected: hasGithubInClerk,
|
||||||
user
|
user
|
||||||
|
|||||||
@ -37,13 +37,13 @@ export function useRepoData(repoId: string): UseRepoDataReturn {
|
|||||||
try {
|
try {
|
||||||
// Check if user has connected GitHub account
|
// Check if user has connected GitHub account
|
||||||
const githubAccount = user.externalAccounts.find(
|
const githubAccount = user.externalAccounts.find(
|
||||||
account => account.provider === 'github'
|
account => account.provider === 'oauth_github'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (githubAccount) {
|
if (githubAccount) {
|
||||||
// Try to get GitHub OAuth token from Clerk
|
// Try to get GitHub OAuth token from Clerk
|
||||||
try {
|
try {
|
||||||
// token = await user.getToken({ template: 'github' });
|
token = await user.getToken({ template: 'github' });
|
||||||
console.log('Using GitHub token from Clerk');
|
console.log('Using GitHub token from Clerk');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error getting GitHub token from Clerk:', err);
|
console.error('Error getting GitHub token from Clerk:', err);
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
// src/hooks/useRepoSelection.ts
|
// src/hooks/useRepoSelection.ts
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useOctokit } from '@/context/OctokitContext'
|
import { useOctokit } from '@/context/OctokitContext'
|
||||||
import { adaptRepositories, type GitHubRepo } from '../utils/typeAdapters';
|
|
||||||
|
|
||||||
interface Repository {
|
interface Repository {
|
||||||
id: string;
|
id: string;
|
||||||
@ -59,7 +58,7 @@ export function useRepoSelection(): UseRepoSelectionResult {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setRepositories(adaptRepositories(repoData as GitHubRepo[]));
|
setRepositories(repoData);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch repositories:', err);
|
console.error('Failed to fetch repositories:', err);
|
||||||
@ -90,9 +89,6 @@ export function useRepoSelection(): UseRepoSelectionResult {
|
|||||||
try {
|
try {
|
||||||
if (isAuth && octokit) {
|
if (isAuth && octokit) {
|
||||||
const [owner, repo] = repoFullName.split('/');
|
const [owner, repo] = repoFullName.split('/');
|
||||||
if (!owner || !repo) {
|
|
||||||
throw new Error('Invalid repository format');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await octokit.request('GET /repos/{owner}/{repo}/branches', {
|
const response = await octokit.request('GET /repos/{owner}/{repo}/branches', {
|
||||||
owner,
|
owner,
|
||||||
|
|||||||
@ -4,7 +4,6 @@ export interface Deployment {
|
|||||||
status: string
|
status: string
|
||||||
isCurrent: boolean
|
isCurrent: boolean
|
||||||
createdAt: string | number
|
createdAt: string | number
|
||||||
commit?: string
|
|
||||||
createdBy?: {
|
createdBy?: {
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
@ -19,17 +18,4 @@ export interface Domain {
|
|||||||
branch: string
|
branch: string
|
||||||
status: string
|
status: string
|
||||||
createdAt: string | number
|
createdAt: string | number
|
||||||
isCustom: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing types that are referenced in components
|
|
||||||
export interface EnvVarItem {
|
|
||||||
key?: string
|
|
||||||
value?: string
|
|
||||||
environments?: string[]
|
|
||||||
isEditing?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EditableEnvVarItem = Partial<EnvVarItem> & {
|
|
||||||
isEditing?: boolean
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
import type { LucideIcon } from 'lucide-react'
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
|
|
||||||
// Page action type (for header actions)
|
|
||||||
export interface PageAction {
|
|
||||||
label: string
|
|
||||||
href: string
|
|
||||||
icon?: string | LucideIcon // Add missing icon property
|
|
||||||
external?: boolean
|
|
||||||
onClick?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page header configuration
|
|
||||||
export interface PageHeaderProps {
|
|
||||||
title: string
|
|
||||||
description?: string // Add missing description property
|
|
||||||
actions?: PageAction[]
|
|
||||||
breadcrumbs?: Array<{
|
|
||||||
label: string
|
|
||||||
href?: string
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page wrapper layout types
|
|
||||||
export type PageLayout = 'default' | 'bento' | 'centered'
|
|
||||||
|
|
||||||
export interface PageWrapperProps {
|
|
||||||
children: ReactNode
|
|
||||||
header?: PageHeaderProps
|
|
||||||
layout?: PageLayout
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dropdown component types
|
|
||||||
export interface DropdownOption {
|
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DropdownProps {
|
|
||||||
label?: string
|
|
||||||
options: DropdownOption[]
|
|
||||||
selectedValue?: string | undefined
|
|
||||||
onSelect: (value: string) => void
|
|
||||||
placeholder?: string
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step header types (for onboarding)
|
|
||||||
export interface StepHeaderProps {
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
icon?: ReactNode // Required icon property
|
|
||||||
}
|
|
||||||
@ -1,6 +1,2 @@
|
|||||||
export * from './deployment'
|
export * from './deployment'
|
||||||
export * from './project'
|
export * from './project'
|
||||||
export * from './dashboard'
|
|
||||||
export * from './common'
|
|
||||||
export * from './foundation'
|
|
||||||
export * from './onboarding'
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
// Template type for onboarding
|
|
||||||
export interface Template {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description: string // Required string
|
|
||||||
repoFullName: string // Required string
|
|
||||||
framework?: string
|
|
||||||
icon?: string // Optional string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deployer type (compatible with GraphQL client)
|
|
||||||
export interface Deployer {
|
|
||||||
deployerLrn: string
|
|
||||||
deploymentLrn: string
|
|
||||||
name: string
|
|
||||||
baseDomain: string
|
|
||||||
minimumPayment?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Organization type
|
|
||||||
export interface Organization {
|
|
||||||
id: string
|
|
||||||
slug: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Environment variable type for onboarding
|
|
||||||
export interface OnboardingEnvVar {
|
|
||||||
key: string
|
|
||||||
value: string
|
|
||||||
environments: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete onboarding form data interface
|
|
||||||
export interface OnboardingFormData {
|
|
||||||
// Step 1: Connect (Repository or Template selection)
|
|
||||||
githubRepo?: string
|
|
||||||
template?: Template
|
|
||||||
projectName?: string
|
|
||||||
deploymentMode?: 'repository' | 'template'
|
|
||||||
|
|
||||||
// Step 2: Configure
|
|
||||||
selectedLrn?: string
|
|
||||||
selectedOrg?: string // Add the missing selectedOrg property
|
|
||||||
environmentVariables?: OnboardingEnvVar[] // Use proper array type
|
|
||||||
deploymentType?: 'auction' | 'lrn' // Add missing properties
|
|
||||||
deployerCount?: string
|
|
||||||
maxPrice?: string
|
|
||||||
paymentAddress?: string // Add missing paymentAddress property
|
|
||||||
|
|
||||||
// Step 3: Deploy
|
|
||||||
deploymentId?: string
|
|
||||||
repositoryUrl?: string // Add missing repositoryUrl property
|
|
||||||
deploymentUrl?: string // Add missing deploymentUrl property
|
|
||||||
projectId?: string // Add missing projectId property
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deployment configuration types
|
|
||||||
export interface TemplateDeploymentConfig {
|
|
||||||
template: Template
|
|
||||||
projectName: string
|
|
||||||
organizationSlug: string
|
|
||||||
environmentVariables?: OnboardingEnvVar[]
|
|
||||||
deployerLrn: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeploymentConfig {
|
|
||||||
projectId: string
|
|
||||||
organizationSlug: string
|
|
||||||
repository: string
|
|
||||||
branch: string
|
|
||||||
name: string
|
|
||||||
environmentVariables?: OnboardingEnvVar[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repository type for GitHub integration
|
|
||||||
export interface Repository {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
full_name: string
|
|
||||||
default_branch: string
|
|
||||||
html_url: string
|
|
||||||
description?: string | null
|
|
||||||
owner: {
|
|
||||||
login: string
|
|
||||||
avatar_url: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -18,10 +18,3 @@ export interface Project {
|
|||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectStatus {
|
|
||||||
Building = 'Building',
|
|
||||||
Ready = 'Ready',
|
|
||||||
Error = 'Error',
|
|
||||||
Deleting = 'Deleting'
|
|
||||||
}
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
// src/utils/typeAdapters.ts
|
|
||||||
import {
|
|
||||||
Deployer as GQLDeployer,
|
|
||||||
DeploymentStatus } from '../../../../services/gql-client/src/types';
|
|
||||||
|
|
||||||
// Define your frontend types that may be missing
|
|
||||||
export type ProjectStatus = 'success' | 'pending' | 'error' | 'building' | 'deleting';
|
|
||||||
|
|
||||||
export interface TemplateDetail {
|
|
||||||
id?: string;
|
|
||||||
name: string;
|
|
||||||
icon: string;
|
|
||||||
description?: string;
|
|
||||||
repoFullName?: string;
|
|
||||||
[key: string]: any; // Allow other properties
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Template {
|
|
||||||
id?: string;
|
|
||||||
name: string;
|
|
||||||
icon?: string;
|
|
||||||
description?: string;
|
|
||||||
repoFullName?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Repository {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
full_name: string;
|
|
||||||
default_branch: string;
|
|
||||||
html_url: string;
|
|
||||||
description?: string; // Note: undefined, not null
|
|
||||||
owner: {
|
|
||||||
login: string;
|
|
||||||
avatar_url: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Deployer {
|
|
||||||
deployerLrn: string;
|
|
||||||
deployerId: string;
|
|
||||||
deployerApiUrl: string;
|
|
||||||
baseDomain: string;
|
|
||||||
minimumPayment?: string; // Note: undefined, not null
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GitHubRepo {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
full_name: string;
|
|
||||||
default_branch: string;
|
|
||||||
html_url: string;
|
|
||||||
description: string | null;
|
|
||||||
owner: {
|
|
||||||
login: string;
|
|
||||||
avatar_url: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert GraphQL Deployer to Frontend Deployer
|
|
||||||
*/
|
|
||||||
export const adaptDeployer = (gqlDeployer: GQLDeployer): Deployer => ({
|
|
||||||
...gqlDeployer,
|
|
||||||
minimumPayment: gqlDeployer.minimumPayment ?? undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert array of GraphQL Deployers to Frontend Deployers
|
|
||||||
*/
|
|
||||||
export const adaptDeployers = (gqlDeployers: GQLDeployer[]): Deployer[] => {
|
|
||||||
return gqlDeployers.map(adaptDeployer);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert Template to TemplateDetail (ensuring icon is always string)
|
|
||||||
*/
|
|
||||||
export const adaptTemplate = (template: Template): TemplateDetail => ({
|
|
||||||
...template,
|
|
||||||
icon: template.icon || '/default-template-icon.svg' // Provide a default icon path
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert DeploymentStatus to ProjectStatus
|
|
||||||
*/
|
|
||||||
export const adaptDeploymentStatus = (status: string | DeploymentStatus): ProjectStatus => {
|
|
||||||
// Convert to string safely
|
|
||||||
const statusStr = String(status);
|
|
||||||
|
|
||||||
switch (statusStr) {
|
|
||||||
case 'Building':
|
|
||||||
case 'building':
|
|
||||||
return 'building';
|
|
||||||
case 'Ready':
|
|
||||||
case 'ready':
|
|
||||||
case 'complete':
|
|
||||||
return 'success';
|
|
||||||
case 'Error':
|
|
||||||
case 'error':
|
|
||||||
case 'failed':
|
|
||||||
return 'error';
|
|
||||||
case 'Deleting':
|
|
||||||
case 'deleting':
|
|
||||||
return 'deleting';
|
|
||||||
case 'pending':
|
|
||||||
return 'pending';
|
|
||||||
default:
|
|
||||||
console.warn(`Unknown deployment status: ${statusStr}`);
|
|
||||||
return 'pending';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert GitHub API Repository to Frontend Repository
|
|
||||||
*/
|
|
||||||
export const adaptRepository = (githubRepo: GitHubRepo): Repository => ({
|
|
||||||
...githubRepo,
|
|
||||||
description: githubRepo.description ?? undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert array of GitHub repositories
|
|
||||||
*/
|
|
||||||
export const adaptRepositories = (githubRepos: GitHubRepo[]): Repository[] => {
|
|
||||||
return githubRepos.map(adaptRepository);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely extract owner and repo name from selected repository
|
|
||||||
*/
|
|
||||||
export const extractRepoInfo = (selectedRepo: Repository | null | undefined): { owner: string; repo: string } => {
|
|
||||||
if (!selectedRepo) {
|
|
||||||
throw new Error('No repository selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
const owner = selectedRepo.owner?.login;
|
|
||||||
const repo = selectedRepo.name;
|
|
||||||
|
|
||||||
if (!owner || !repo) {
|
|
||||||
throw new Error('Repository owner and name are required');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { owner, repo };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Template deployment configuration adapter
|
|
||||||
*/
|
|
||||||
export interface TemplateDeploymentConfig {
|
|
||||||
template: TemplateDetail;
|
|
||||||
projectName: string;
|
|
||||||
organizationSlug: string;
|
|
||||||
environmentVariables: any[];
|
|
||||||
deployerLrn?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const adaptTemplateDeploymentConfig = (config: {
|
|
||||||
template: Template;
|
|
||||||
projectName: string;
|
|
||||||
organizationSlug: string;
|
|
||||||
environmentVariables: any[];
|
|
||||||
deployerLrn?: string;
|
|
||||||
}): TemplateDeploymentConfig => ({
|
|
||||||
...config,
|
|
||||||
template: adaptTemplate(config.template)
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to handle optional template conversion
|
|
||||||
*/
|
|
||||||
export const adaptOptionalTemplate = (template: Template | undefined): TemplateDetail | undefined => {
|
|
||||||
return template ? adaptTemplate(template) : undefined;
|
|
||||||
};
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
REGISTRY_BOND_ID=
|
|
||||||
DEPLOYER_LRN=
|
|
||||||
AUTHORITY=
|
|
||||||
@ -1,9 +1,5 @@
|
|||||||
# deployer
|
# deployer
|
||||||
|
|
||||||
The following steps are for deploying the frontend app to vaasl webapp deployer.
|
|
||||||
|
|
||||||
Run these steps in the `apps/deployer/` directory:
|
|
||||||
|
|
||||||
- Install dependencies
|
- Install dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -11,45 +7,18 @@ Run these steps in the `apps/deployer/` directory:
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# if you do not have jq installed already
|
brew install jq # if you do not have jq installed already
|
||||||
brew install jq
|
|
||||||
# OR
|
|
||||||
sudo apt-get install jq
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Copy and update `.env`
|
- Run script to deploy app
|
||||||
|
|
||||||
```
|
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
|
||||||
cp .env.example .env
|
|
||||||
|
```bash
|
||||||
|
./deploy-frontend.staging.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Set the required variables:
|
- To deploy frontend app to `dashboard.apps.snowballtools.com`
|
||||||
|
|
||||||
```
|
|
||||||
REGISTRY_BOND_ID=230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38
|
|
||||||
DEPLOYER_LRN=lrn://vaasl-provider/deployers/webapp-deployer-api.apps.vaasl.io
|
|
||||||
AUTHORITY=laconic
|
|
||||||
```
|
|
||||||
|
|
||||||
- Set the `userKey` in [config.yml](./config.yml)
|
|
||||||
|
|
||||||
Note: The `userKey` should be the private key of the account owning the bond set in `config.yml` (account address: `laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef`)
|
|
||||||
|
|
||||||
- Set the app env values in [deploy-frontend.sh](./deploy-frontend.sh) (Refer to main [readme](/readme.md#3-configure-environment-variables)):
|
|
||||||
<!-- TODO: Set env values from a file -->
|
|
||||||
|
|
||||||
```sh
|
|
||||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: your_clerk_key
|
|
||||||
CLERK_SECRET_KEY: your_clerk_secret
|
|
||||||
NEXT_PUBLIC_WALLET_IFRAME_URL: https://wallet.laconic.com
|
|
||||||
NEXT_PUBLIC_LACONICD_CHAIN_ID: laconic-mainnet
|
|
||||||
NEXT_PUBLIC_API_URL: https://deploy-backend.apps.vaasl.io
|
|
||||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN: your_github_token
|
|
||||||
```
|
|
||||||
|
|
||||||
NOTE: Replace `your_clerk_key`, `your_clerk_secret` and `your_github_token` with actual values
|
|
||||||
|
|
||||||
- Run script to deploy app to `deploy-staging.laconic.co`
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./deploy-frontend.sh
|
./deploy-frontend.sh
|
||||||
@ -57,14 +26,27 @@ Run these steps in the `apps/deployer/` directory:
|
|||||||
|
|
||||||
- Commit the updated [ApplicationRecord](records/application-record.yml) and [ApplicationDeploymentRequest](records/application-deployment-request.yml) files to the repository
|
- Commit the updated [ApplicationRecord](records/application-record.yml) and [ApplicationDeploymentRequest](records/application-deployment-request.yml) files to the repository
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||||
|
|
||||||
|
```yml
|
||||||
|
record:
|
||||||
|
...
|
||||||
|
config:
|
||||||
|
env:
|
||||||
|
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
|
||||||
|
|
||||||
## Troubleshoot
|
## Troubleshoot
|
||||||
|
|
||||||
- Check deployment status in [web-app deployer](https://console.laconic.com/deployer).
|
- Check deployment status in [web-app deployer](https://console.laconic.com/deployer).
|
||||||
|
|
||||||
- Check records in [registry console app](https://console.laconic.com/#/registry).
|
- Check records in [registry console app](https://console.laconic.com/#/registry).
|
||||||
|
|
||||||
- If deployment fails due to low bond balance
|
- If deployment fails due to low bond balance
|
||||||
|
|
||||||
- Check balances
|
- Check balances
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -72,11 +54,11 @@ Run these steps in the `apps/deployer/` directory:
|
|||||||
pnpm laconic registry account get
|
pnpm laconic registry account get
|
||||||
|
|
||||||
# Bond balance
|
# Bond balance
|
||||||
pnpm laconic registry bond get --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38
|
pnpm laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||||
```
|
```
|
||||||
|
|
||||||
- Command to refill bond
|
- Command to refill bond
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm laconic registry bond refill --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38 --type alnt --quantity 10000000
|
pnpm laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
registry:
|
registry:
|
||||||
rpcEndpoint: https://laconicd-mainnet-1.laconic.com/
|
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||||
gqlEndpoint: https://laconicd-mainnet-1.laconic.com/api
|
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||||
userKey:
|
userKey:
|
||||||
bondId: 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38
|
bondId:
|
||||||
chainId: laconic-mainnet
|
chainId: laconic-testnet-2
|
||||||
gasPrice: 0.001alnt
|
gasPrice: 0.001alnt
|
||||||
|
|||||||
@ -6,7 +6,7 @@ echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
|||||||
echo "Using AUTHORITY: $AUTHORITY"
|
echo "Using AUTHORITY: $AUTHORITY"
|
||||||
|
|
||||||
# Repository URL
|
# Repository URL
|
||||||
REPO_URL="https://git.vdb.to/NasSharaf/laconic-deployer-frontend"
|
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
|
|
||||||
# Get the latest commit hash for a branch
|
# Get the latest commit hash for a branch
|
||||||
BRANCH_NAME="main"
|
BRANCH_NAME="main"
|
||||||
@ -25,7 +25,7 @@ CONFIG_FILE=config.yml
|
|||||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||||
|
|
||||||
# Get latest version from registry and increment application-record version
|
# Get latest version from registry and increment application-record version
|
||||||
NEW_APPLICATION_VERSION=$(pnpm --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "laconic-deployer-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
NEW_APPLICATION_VERSION=$(pnpm --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "deploy-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||||
|
|
||||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||||
# Set application-record version if no previous records were found
|
# Set application-record version if no previous records were found
|
||||||
@ -39,8 +39,8 @@ record:
|
|||||||
version: $NEW_APPLICATION_VERSION
|
version: $NEW_APPLICATION_VERSION
|
||||||
repository_ref: $LATEST_HASH
|
repository_ref: $LATEST_HASH
|
||||||
repository: ["$REPO_URL"]
|
repository: ["$REPO_URL"]
|
||||||
app_type: webapp/next
|
app_type: webapp
|
||||||
name: laconic-deployer-frontend
|
name: deploy-frontend
|
||||||
app_version: $PACKAGE_VERSION
|
app_version: $PACKAGE_VERSION
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ echo "ApplicationRecord published"
|
|||||||
echo $RECORD_ID
|
echo $RECORD_ID
|
||||||
|
|
||||||
# Set name to record
|
# Set name to record
|
||||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/laconic-deployer-frontend"
|
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||||
@ -122,18 +122,19 @@ cat >./records/application-deployment-request.yml <<EOF
|
|||||||
record:
|
record:
|
||||||
type: ApplicationDeploymentRequest
|
type: ApplicationDeploymentRequest
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
name: laconic-deployer-frontend@$PACKAGE_VERSION
|
name: deploy-frontend@$PACKAGE_VERSION
|
||||||
application: lrn://$AUTHORITY/applications/laconic-deployer-frontend@$PACKAGE_VERSION
|
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
||||||
deployer: $DEPLOYER_LRN
|
deployer: $DEPLOYER_LRN
|
||||||
dns: deploy-staging.laconic.com
|
dns: deploy.laconic.com
|
||||||
config:
|
config:
|
||||||
env:
|
env:
|
||||||
NEXT_PUBLIC_WALLET_IFRAME_URL: https://wallet.laconic.com
|
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.laconic.com
|
||||||
NEXT_PUBLIC_LACONICD_CHAIN_ID: laconic-mainnet
|
LACONIC_HOSTED_CONFIG_github_clientid: Ov23li4NtYybQlF6u5Dk
|
||||||
NEXT_PUBLIC_API_URL: https://deploy-backend.apps.vaasl.io
|
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
|
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||||
CLERK_SECRET_KEY:
|
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
||||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN: your_github_token
|
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||||
|
LACONIC_HOSTED_CONFIG_wallet_iframe_url: https://wallet.laconic.com
|
||||||
meta:
|
meta:
|
||||||
note: Added @ $CURRENT_DATE_TIME
|
note: Added @ $CURRENT_DATE_TIME
|
||||||
repository: "$REPO_URL"
|
repository: "$REPO_URL"
|
||||||
@ -141,12 +142,6 @@ record:
|
|||||||
payment: $txHash
|
payment: $txHash
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Old env values for frontend app
|
|
||||||
# LACONIC_HOSTED_CONFIG_github_clientid: Ov23li4NtYybQlF6u5Dk
|
|
||||||
# LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
|
||||||
# LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
|
||||||
# LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
|
||||||
|
|
||||||
RECORD_FILE=records/application-deployment-request.yml
|
RECORD_FILE=records/application-deployment-request.yml
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
PKG_DIR="./apps/deploy-fe"
|
|
||||||
|
|
||||||
cat > $PKG_DIR/.env <<EOF
|
|
||||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=CERC_RUNTIME_ENV_NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
|
|
||||||
CLERK_SECRET_KEY=CERC_RUNTIME_ENV_CLERK_SECRET_KEY
|
|
||||||
NEXT_PUBLIC_WALLET_IFRAME_URL=CERC_RUNTIME_ENV_NEXT_PUBLIC_WALLET_IFRAME_URL
|
|
||||||
NEXT_PUBLIC_LACONICD_CHAIN_ID=CERC_RUNTIME_ENV_NEXT_PUBLIC_LACONICD_CHAIN_ID
|
|
||||||
NEXT_PUBLIC_API_URL=CERC_RUNTIME_ENV_NEXT_PUBLIC_API_URL
|
|
||||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=CERC_RUNTIME_ENV_NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN
|
|
||||||
EOF
|
|
||||||
|
|
||||||
pnpm install || exit 1
|
|
||||||
|
|
||||||
pnpm build:fe-compile || exit 1
|
|
||||||
@ -5,7 +5,6 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo build",
|
"build": "turbo build",
|
||||||
"build:fe-compile": "turbo build:compile --filter=deploy-fe",
|
|
||||||
"dev": "turbo dev",
|
"dev": "turbo dev",
|
||||||
"lint": "turbo lint",
|
"lint": "turbo lint",
|
||||||
"start": "turbo start",
|
"start": "turbo start",
|
||||||
|
|||||||
2073
pnpm-lock.yaml
generated
2073
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
24
readme.md
24
readme.md
@ -41,7 +41,7 @@ Create a `.env.local` file in the `apps/deploy-fe` directory:
|
|||||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_key
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_key
|
||||||
CLERK_SECRET_KEY=your_clerk_secret
|
CLERK_SECRET_KEY=your_clerk_secret
|
||||||
NEXT_PUBLIC_WALLET_IFRAME_URL=http://localhost:4000
|
NEXT_PUBLIC_WALLET_IFRAME_URL=http://localhost:4000
|
||||||
NEXT_PUBLIC_LACONICD_CHAIN_ID=laconic-mainnet
|
NEXT_PUBLIC_LACONICD_CHAIN_ID=laconic-testnet-2
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=your_github_token
|
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=your_github_token
|
||||||
```
|
```
|
||||||
@ -93,25 +93,3 @@ Make sure you have:
|
|||||||
1. Connected your GitHub account in Clerk
|
1. Connected your GitHub account in Clerk
|
||||||
2. Provided the necessary permissions for repository access
|
2. Provided the necessary permissions for repository access
|
||||||
3. Set a fallback GitHub token in `.env.local` if needed for development
|
3. Set a fallback GitHub token in `.env.local` if needed for development
|
||||||
|
|
||||||
## Test deployment
|
|
||||||
|
|
||||||
- Follow the dev install steps for stack-orchestrator from <https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/CONTRIBUTING.md#install>
|
|
||||||
|
|
||||||
- Build the container for app
|
|
||||||
|
|
||||||
```bash
|
|
||||||
laconic-so build-webapp --source-repo <path-to-app-repo>/laconic-deployer-frontend --base-container cerc/nextjs-base
|
|
||||||
```
|
|
||||||
|
|
||||||
- Create an env file as described in [previous steps](#3-configure-environment-variables)
|
|
||||||
|
|
||||||
- Run the webapp container locally
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -p 3000:80 --env-file <path-to-env-file> cerc/laconic-deployer-frontend:local
|
|
||||||
```
|
|
||||||
|
|
||||||
- Scripts used for deployment
|
|
||||||
- App container build : [build-webapp.sh](./build-webapp.sh)
|
|
||||||
- Run app container : [run-webapp.sh](./run-webapp.sh)
|
|
||||||
|
|||||||
@ -1,396 +0,0 @@
|
|||||||
./.github/CONTRIBUTING.md
|
|
||||||
./.turbo/cache/88602030b8f39256-meta.json
|
|
||||||
./.turbo/cache/981738a1c69b2e12-meta.json
|
|
||||||
./.turbo/cache/cff97096ef4824bd-meta.json
|
|
||||||
./.turbo/preferences/tui.json
|
|
||||||
./.vscode/extensions.json
|
|
||||||
./.vscode/settings.json
|
|
||||||
./BACKEND_CONNECTION.md
|
|
||||||
./apps/backend/README.md
|
|
||||||
./apps/backend/biome.json
|
|
||||||
./apps/backend/dist/config.d.ts
|
|
||||||
./apps/backend/dist/constants.d.ts
|
|
||||||
./apps/backend/dist/database.d.ts
|
|
||||||
./apps/backend/dist/entity/Deployer.d.ts
|
|
||||||
./apps/backend/dist/entity/Deployment.d.ts
|
|
||||||
./apps/backend/dist/entity/Domain.d.ts
|
|
||||||
./apps/backend/dist/entity/EnvironmentVariable.d.ts
|
|
||||||
./apps/backend/dist/entity/Organization.d.ts
|
|
||||||
./apps/backend/dist/entity/Project.d.ts
|
|
||||||
./apps/backend/dist/entity/ProjectMember.d.ts
|
|
||||||
./apps/backend/dist/entity/User.d.ts
|
|
||||||
./apps/backend/dist/entity/UserOrganization.d.ts
|
|
||||||
./apps/backend/dist/index.d.ts
|
|
||||||
./apps/backend/dist/registry.d.ts
|
|
||||||
./apps/backend/dist/resolvers.d.ts
|
|
||||||
./apps/backend/dist/routes/auth.d.ts
|
|
||||||
./apps/backend/dist/routes/github.d.ts
|
|
||||||
./apps/backend/dist/routes/staging.d.ts
|
|
||||||
./apps/backend/dist/server.d.ts
|
|
||||||
./apps/backend/dist/service.d.ts
|
|
||||||
./apps/backend/dist/turnkey-backend.d.ts
|
|
||||||
./apps/backend/dist/types.d.ts
|
|
||||||
./apps/backend/dist/utils.d.ts
|
|
||||||
./apps/backend/environments/local.toml
|
|
||||||
./apps/backend/package.json
|
|
||||||
./apps/backend/src/config.ts
|
|
||||||
./apps/backend/src/constants.ts
|
|
||||||
./apps/backend/src/database.ts
|
|
||||||
./apps/backend/src/entity/Deployer.ts
|
|
||||||
./apps/backend/src/entity/Deployment.ts
|
|
||||||
./apps/backend/src/entity/Domain.ts
|
|
||||||
./apps/backend/src/entity/EnvironmentVariable.ts
|
|
||||||
./apps/backend/src/entity/Organization.ts
|
|
||||||
./apps/backend/src/entity/Project.ts
|
|
||||||
./apps/backend/src/entity/ProjectMember.ts
|
|
||||||
./apps/backend/src/entity/User.ts
|
|
||||||
./apps/backend/src/entity/UserOrganization.ts
|
|
||||||
./apps/backend/src/index.ts
|
|
||||||
./apps/backend/src/registry.ts
|
|
||||||
./apps/backend/src/resolvers.ts
|
|
||||||
./apps/backend/src/routes/auth.ts
|
|
||||||
./apps/backend/src/routes/github.ts
|
|
||||||
./apps/backend/src/routes/staging.ts
|
|
||||||
./apps/backend/src/server.ts
|
|
||||||
./apps/backend/src/service.ts
|
|
||||||
./apps/backend/src/turnkey-backend.ts
|
|
||||||
./apps/backend/src/types.ts
|
|
||||||
./apps/backend/src/utils.ts
|
|
||||||
./apps/backend/test/delete-db.ts
|
|
||||||
./apps/backend/test/fixtures/deployments.json
|
|
||||||
./apps/backend/test/fixtures/environment-variables.json
|
|
||||||
./apps/backend/test/fixtures/organizations.json
|
|
||||||
./apps/backend/test/fixtures/primary-domains.json
|
|
||||||
./apps/backend/test/fixtures/project-members.json
|
|
||||||
./apps/backend/test/fixtures/projects.json
|
|
||||||
./apps/backend/test/fixtures/redirected-domains.json
|
|
||||||
./apps/backend/test/fixtures/user-organizations.json
|
|
||||||
./apps/backend/test/fixtures/users.json
|
|
||||||
./apps/backend/test/initialize-db.ts
|
|
||||||
./apps/backend/test/initialize-registry.ts
|
|
||||||
./apps/backend/test/publish-deploy-records.ts
|
|
||||||
./apps/backend/test/publish-deployment-removal-records.ts
|
|
||||||
./apps/backend/tsconfig.json
|
|
||||||
./apps/deploy-fe/.vscode/settings.json
|
|
||||||
./apps/deploy-fe/components.json
|
|
||||||
./apps/deploy-fe/next-env.d.ts
|
|
||||||
./apps/deploy-fe/package.json
|
|
||||||
./apps/deploy-fe/src/actions/github.ts
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/documentation/DocumentationPlaceholder.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/documentation/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/home/loading.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/home/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/layout.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/(template)/tm/(configure)/cf/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/(template)/tm/(deploy)/dp/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(deployments)/dep/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(integrations)/int/GitPage.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(integrations)/int/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(collaborators)/col/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(domains)/dom/(add)/cf/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(domains)/dom/(add)/config/cf/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(environment-variables)/env/EnvVarsPage.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(environment-variables)/env/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(git)/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/ProjectSettingsPage.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/deployments/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/layout.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/loading.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/error.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/loading.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/purchase/BuyServices.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/purchase/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/store/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/support/SupportPlaceholder.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/support/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/wallet/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/(web3-authenticated)/layout.tsx
|
|
||||||
./apps/deploy-fe/src/app/actions/github.ts
|
|
||||||
./apps/deploy-fe/src/app/api/auth/route.ts
|
|
||||||
./apps/deploy-fe/src/app/api/github/webhook/route.ts
|
|
||||||
./apps/deploy-fe/src/app/auth/github/backend-callback/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/layout.tsx
|
|
||||||
./apps/deploy-fe/src/app/loading.tsx
|
|
||||||
./apps/deploy-fe/src/app/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/sign-in/[[...sign-in]]/page.tsx
|
|
||||||
./apps/deploy-fe/src/app/test-connection/page.tsx
|
|
||||||
./apps/deploy-fe/src/components/AuthTest.tsx
|
|
||||||
./apps/deploy-fe/src/components/DeploymentTest.tsx
|
|
||||||
./apps/deploy-fe/src/components/DevAuth.tsx
|
|
||||||
./apps/deploy-fe/src/components/DirectKeyAuth.tsx
|
|
||||||
./apps/deploy-fe/src/components/GItHubBackendAuth.tsx
|
|
||||||
./apps/deploy-fe/src/components/GQLTest.tsx
|
|
||||||
./apps/deploy-fe/src/components/TurnkeyAuth.tsx
|
|
||||||
./apps/deploy-fe/src/components/WalletConnectAuth.tsx
|
|
||||||
./apps/deploy-fe/src/components/assets/laconic-mark.tsx
|
|
||||||
./apps/deploy-fe/src/components/core/dropdown/Dropdown.tsx
|
|
||||||
./apps/deploy-fe/src/components/core/dropdown/README.md
|
|
||||||
./apps/deploy-fe/src/components/core/dropdown/index.ts
|
|
||||||
./apps/deploy-fe/src/components/core/dropdown/types.ts
|
|
||||||
./apps/deploy-fe/src/components/core/format-milli-second/FormatMilliSecond.tsx
|
|
||||||
./apps/deploy-fe/src/components/core/format-milli-second/README.md
|
|
||||||
./apps/deploy-fe/src/components/core/format-milli-second/index.ts
|
|
||||||
./apps/deploy-fe/src/components/core/format-milli-second/types.ts
|
|
||||||
./apps/deploy-fe/src/components/core/logo/Logo.tsx
|
|
||||||
./apps/deploy-fe/src/components/core/logo/README.md
|
|
||||||
./apps/deploy-fe/src/components/core/logo/index.ts
|
|
||||||
./apps/deploy-fe/src/components/core/logo/types.ts
|
|
||||||
./apps/deploy-fe/src/components/core/search-bar/README.md
|
|
||||||
./apps/deploy-fe/src/components/core/search-bar/SearchBar.tsx
|
|
||||||
./apps/deploy-fe/src/components/core/search-bar/index.ts
|
|
||||||
./apps/deploy-fe/src/components/core/search-bar/types.ts
|
|
||||||
./apps/deploy-fe/src/components/core/stepper/README.md
|
|
||||||
./apps/deploy-fe/src/components/core/stepper/Stepper.tsx
|
|
||||||
./apps/deploy-fe/src/components/core/stepper/index.ts
|
|
||||||
./apps/deploy-fe/src/components/core/stepper/types.ts
|
|
||||||
./apps/deploy-fe/src/components/core/stop-watch/README.md
|
|
||||||
./apps/deploy-fe/src/components/core/stop-watch/StopWatch.tsx
|
|
||||||
./apps/deploy-fe/src/components/core/stop-watch/index.ts
|
|
||||||
./apps/deploy-fe/src/components/core/stop-watch/types.ts
|
|
||||||
./apps/deploy-fe/src/components/core/vertical-stepper/README.md
|
|
||||||
./apps/deploy-fe/src/components/core/vertical-stepper/VerticalStepper.tsx
|
|
||||||
./apps/deploy-fe/src/components/core/vertical-stepper/index.ts
|
|
||||||
./apps/deploy-fe/src/components/core/vertical-stepper/types.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/coming-soon-overlay/ComingSoonOverlay.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/coming-soon-overlay/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/github-session-button/GitHubSessionButton.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/github-session-button/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/github-session-button/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/github-session-button/types.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/laconic-icon/LaconicIcon.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/laconic-icon/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/laconic-icon/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/laconic-icon/types.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/loading/loading-overlay/LoadingOverlay.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/loading/loading-overlay/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/loading/loading-overlay/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/navigation-wrapper/NavigationWrapper.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/navigation-wrapper/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/navigation-wrapper/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/page-header/PageHeader.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/page-header/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/page-header/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/page-wrapper/PageWrapper.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/page-wrapper/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/page-wrapper/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/project-search-bar/ProjectSearchBar.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/project-search-bar/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/project-search-bar/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/project-search-bar/types.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/dark-mode-toggle/DarkModeToggle.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/dark-mode-toggle/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/dark-mode-toggle/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/main-navigation/MainNavigation.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/main-navigation/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/main-navigation/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/navigation-item/NavigationItem.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/navigation-item/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/navigation-item/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/types.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/wallet-session-badge/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/wallet-session-badge/WalletSessionBadge.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/top-navigation/wallet-session-badge/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/types.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/wallet-session-id/README.md
|
|
||||||
./apps/deploy-fe/src/components/foundation/wallet-session-id/WalletSessionId.tsx
|
|
||||||
./apps/deploy-fe/src/components/foundation/wallet-session-id/index.ts
|
|
||||||
./apps/deploy-fe/src/components/foundation/wallet-session-id/types.ts
|
|
||||||
./apps/deploy-fe/src/components/iframe/auto-sign-in/AutoSignInIFrameModal.tsx
|
|
||||||
./apps/deploy-fe/src/components/iframe/auto-sign-in/README.md
|
|
||||||
./apps/deploy-fe/src/components/iframe/auto-sign-in/index.ts
|
|
||||||
./apps/deploy-fe/src/components/iframe/auto-sign-in/types.ts
|
|
||||||
./apps/deploy-fe/src/components/iframe/check-balance-iframe/CheckBalanceIframe.tsx
|
|
||||||
./apps/deploy-fe/src/components/iframe/check-balance-iframe/CheckBalanceWrapper.tsx
|
|
||||||
./apps/deploy-fe/src/components/iframe/check-balance-iframe/useCheckBalance.tsx
|
|
||||||
./apps/deploy-fe/src/components/layout/index.ts
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/github-session-button/GitHubSessionButton.tsx
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/github-session-button/README.md
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/github-session-button/index.ts
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/github-session-button/types.ts
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/laconic-icon/LaconicIcon.tsx
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/laconic-icon/README.md
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/laconic-icon/index.ts
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/laconic-icon/types.ts
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/navigation-actions/NavigationActions.tsx
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/navigation-actions/README.md
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/navigation-actions/index.ts
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/navigation-actions/types.ts
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/wallet-session-id/README.md
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/wallet-session-id/WalletSessionId.tsx
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/wallet-session-id/index.ts
|
|
||||||
./apps/deploy-fe/src/components/layout/navigation/wallet-session-id/types.ts
|
|
||||||
./apps/deploy-fe/src/components/loading/loading-overlay.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/OPTIMIZATION.md
|
|
||||||
./apps/deploy-fe/src/components/onboarding/Onboarding.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/OnboardingButton.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/OnboardingDialog.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/OnboardingSidebar.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/README.md
|
|
||||||
./apps/deploy-fe/src/components/onboarding/common/background-svg.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/common/index.ts
|
|
||||||
./apps/deploy-fe/src/components/onboarding/common/laconic-icon-lettering.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/common/onboarding-container.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/common/step-header.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/common/step-navigation.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/configure-step/configure-step.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/configure-step/disable_configure-step.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/configure-step/index.ts
|
|
||||||
./apps/deploy-fe/src/components/onboarding/connect-step/connect-button.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/connect-step/connect-deploy-first-app.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/connect-step/connect-initial.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/connect-step/connect-step.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/connect-step/disabled_connect-step.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/connect-step/index.ts
|
|
||||||
./apps/deploy-fe/src/components/onboarding/connect-step/repository-list.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/connect-step/template-list.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/deploy-step/deploy-step.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/deploy-step/disabled_deploy-step.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/deploy-step/index.ts
|
|
||||||
./apps/deploy-fe/src/components/onboarding/index.ts
|
|
||||||
./apps/deploy-fe/src/components/onboarding/sidebar/index.ts
|
|
||||||
./apps/deploy-fe/src/components/onboarding/sidebar/sidebar-nav.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/store.ts
|
|
||||||
./apps/deploy-fe/src/components/onboarding/success-step/success-step.tsx
|
|
||||||
./apps/deploy-fe/src/components/onboarding/types.ts
|
|
||||||
./apps/deploy-fe/src/components/onboarding/useOnboarding.ts
|
|
||||||
./apps/deploy-fe/src/components/projects/project/ProjectCard/FixedProjectCard.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/ProjectCard/ProjectCard.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/ProjectCard/ProjectCardActions.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/ProjectCard/ProjectDeploymentInfo.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/ProjectCard/ProjectStatusDot.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/ProjectCard/index.ts
|
|
||||||
./apps/deploy-fe/src/components/projects/project/ProjectSearchBar/ProjectSearchBar.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/ProjectSearchBar/ProjectSearchBarDialog.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/ProjectSearchBar/ProjectSearchBarEmpty.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/ProjectSearchBar/ProjectSearchBarItem.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/ProjectSearchBar/index.ts
|
|
||||||
./apps/deploy-fe/src/components/projects/project/deployments/DeploymentDetailsCard.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/deployments/FilterForm.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/overview/Activity/AuctionCard.tsx
|
|
||||||
./apps/deploy-fe/src/components/projects/project/overview/OverviewInfo.tsx
|
|
||||||
./apps/deploy-fe/src/components/providers.tsx
|
|
||||||
./apps/deploy-fe/src/context/GQLClientContext.tsx
|
|
||||||
./apps/deploy-fe/src/context/OctokitContext.tsx
|
|
||||||
./apps/deploy-fe/src/context/OctokitProviderWithRouter.tsx
|
|
||||||
./apps/deploy-fe/src/context/WalletContext.tsx
|
|
||||||
./apps/deploy-fe/src/context/WalletContextProvider.tsx
|
|
||||||
./apps/deploy-fe/src/context/index.ts
|
|
||||||
./apps/deploy-fe/src/hooks/disabled_useDeployment.tsx
|
|
||||||
./apps/deploy-fe/src/hooks/disabled_useRepoData.tsx
|
|
||||||
./apps/deploy-fe/src/hooks/useDeployment.tsx
|
|
||||||
./apps/deploy-fe/src/hooks/useRepoData.tsx
|
|
||||||
./apps/deploy-fe/src/hooks/useRepoSelection.tsx
|
|
||||||
./apps/deploy-fe/src/lib/utils.ts
|
|
||||||
./apps/deploy-fe/src/middleware.ts
|
|
||||||
./apps/deploy-fe/src/types/common.ts
|
|
||||||
./apps/deploy-fe/src/types/dashboard.ts
|
|
||||||
./apps/deploy-fe/src/types/deployment.ts
|
|
||||||
./apps/deploy-fe/src/types/hooks/use-mobile.tsx
|
|
||||||
./apps/deploy-fe/src/types/index.ts
|
|
||||||
./apps/deploy-fe/src/types/project.ts
|
|
||||||
./apps/deploy-fe/src/utils/getInitials.ts
|
|
||||||
./apps/deploy-fe/src/utils/time.ts
|
|
||||||
./apps/deploy-fe/standards/architecture/routes.md
|
|
||||||
./apps/deploy-fe/tailwind.config.ts
|
|
||||||
./apps/deploy-fe/tsconfig.json
|
|
||||||
./apps/deployer/README.md
|
|
||||||
./apps/deployer/biome.json
|
|
||||||
./apps/deployer/package.json
|
|
||||||
./apps/deployer/test/README.md
|
|
||||||
./biome.json
|
|
||||||
./docs/architecture/wallet_migration/0-wallet-integration-overview.md
|
|
||||||
./docs/architecture/wallet_migration/1-phase-1-wallet-core.md
|
|
||||||
./docs/architecture/wallet_migration/2-phase-2-wallet-ui.md
|
|
||||||
./docs/architecture/wallet_migration/3-phase-3-clerk-integration.md
|
|
||||||
./next-agent-01.md
|
|
||||||
./package.json
|
|
||||||
./readme.md
|
|
||||||
./scripts/README.md
|
|
||||||
./services/gql-client/biome.json
|
|
||||||
./services/gql-client/dist/index.d.ts
|
|
||||||
./services/gql-client/package.json
|
|
||||||
./services/gql-client/src/client.ts
|
|
||||||
./services/gql-client/src/index.ts
|
|
||||||
./services/gql-client/src/mutations.ts
|
|
||||||
./services/gql-client/src/queries.ts
|
|
||||||
./services/gql-client/src/types.ts
|
|
||||||
./services/gql-client/tsconfig.json
|
|
||||||
./services/gql-client/tsup.config.ts
|
|
||||||
./services/typescript-config/README.md
|
|
||||||
./services/typescript-config/base.json
|
|
||||||
./services/typescript-config/nextjs.json
|
|
||||||
./services/typescript-config/package.json
|
|
||||||
./services/typescript-config/react-library.json
|
|
||||||
./services/ui/components.json
|
|
||||||
./services/ui/package.json
|
|
||||||
./services/ui/src/components/accordion.tsx
|
|
||||||
./services/ui/src/components/alert-dialog.tsx
|
|
||||||
./services/ui/src/components/alert.tsx
|
|
||||||
./services/ui/src/components/aspect-ratio.tsx
|
|
||||||
./services/ui/src/components/avatar.tsx
|
|
||||||
./services/ui/src/components/badge.tsx
|
|
||||||
./services/ui/src/components/breadcrumb.tsx
|
|
||||||
./services/ui/src/components/button.tsx
|
|
||||||
./services/ui/src/components/calendar.tsx
|
|
||||||
./services/ui/src/components/card.tsx
|
|
||||||
./services/ui/src/components/carousel.tsx
|
|
||||||
./services/ui/src/components/chart.tsx
|
|
||||||
./services/ui/src/components/checkbox.tsx
|
|
||||||
./services/ui/src/components/collapsible.tsx
|
|
||||||
./services/ui/src/components/command.tsx
|
|
||||||
./services/ui/src/components/context-menu.tsx
|
|
||||||
./services/ui/src/components/dialog.tsx
|
|
||||||
./services/ui/src/components/drawer.tsx
|
|
||||||
./services/ui/src/components/dropdown-menu.tsx
|
|
||||||
./services/ui/src/components/form.tsx
|
|
||||||
./services/ui/src/components/hover-card.tsx
|
|
||||||
./services/ui/src/components/input-otp.tsx
|
|
||||||
./services/ui/src/components/input.tsx
|
|
||||||
./services/ui/src/components/label.tsx
|
|
||||||
./services/ui/src/components/menubar.tsx
|
|
||||||
./services/ui/src/components/navigation-menu.tsx
|
|
||||||
./services/ui/src/components/pagination.tsx
|
|
||||||
./services/ui/src/components/popover.tsx
|
|
||||||
./services/ui/src/components/progress.tsx
|
|
||||||
./services/ui/src/components/radio-group.tsx
|
|
||||||
./services/ui/src/components/resizable.tsx
|
|
||||||
./services/ui/src/components/scroll-area.tsx
|
|
||||||
./services/ui/src/components/select.tsx
|
|
||||||
./services/ui/src/components/separator.tsx
|
|
||||||
./services/ui/src/components/sheet.tsx
|
|
||||||
./services/ui/src/components/sidebar.tsx
|
|
||||||
./services/ui/src/components/skeleton.tsx
|
|
||||||
./services/ui/src/components/slider.tsx
|
|
||||||
./services/ui/src/components/sonner.tsx
|
|
||||||
./services/ui/src/components/switch.tsx
|
|
||||||
./services/ui/src/components/table.tsx
|
|
||||||
./services/ui/src/components/tabs.tsx
|
|
||||||
./services/ui/src/components/textarea.tsx
|
|
||||||
./services/ui/src/components/toggle-group.tsx
|
|
||||||
./services/ui/src/components/toggle.tsx
|
|
||||||
./services/ui/src/components/tooltip.tsx
|
|
||||||
./services/ui/src/hooks/use-mobile.ts
|
|
||||||
./services/ui/src/lib/utils.ts
|
|
||||||
./services/ui/tailwind.config.ts
|
|
||||||
./services/ui/tsconfig.json
|
|
||||||
./services/ui/tsconfig.lint.json
|
|
||||||
./standards/blueprints/file-migration-list.md
|
|
||||||
./standards/blueprints/next-app-router-structure.md
|
|
||||||
./standards/blueprints/nextjs-templates.md
|
|
||||||
./standards/blueprints/qwrk-laconic-migration-guide.md
|
|
||||||
./standards/current-tech-reference.md
|
|
||||||
./standards/documentation/COMPONENT_DOCUMENTATION.md
|
|
||||||
./standards/documentation/FEATURE_BUILDING.md
|
|
||||||
./standards/documentation/FEATURE_BUILDING_TEMPLATE.md
|
|
||||||
./standards/documentation/README.md
|
|
||||||
./standards/documentation/react-component-conventions.md
|
|
||||||
./tsconfig.base.json
|
|
||||||
./tsconfig.json
|
|
||||||
./turbo.json
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
|
|
||||||
set -x
|
|
||||||
fi
|
|
||||||
|
|
||||||
SCRIPT_DIR=/scripts/
|
|
||||||
|
|
||||||
PKG_DIR="./apps/deploy-fe"
|
|
||||||
cd $PKG_DIR
|
|
||||||
|
|
||||||
"$SCRIPT_DIR/apply-runtime-env.sh" "`pwd`" .next .next-r
|
|
||||||
mv .next .next.old
|
|
||||||
mv .next-r/.next .
|
|
||||||
|
|
||||||
pnpm build:generate || exit 1
|
|
||||||
|
|
||||||
pnpm start -p ${CERC_LISTEN_PORT:-80}
|
|
||||||
@ -8,11 +8,6 @@
|
|||||||
"outputs": [".next/**", "!.next/cache/**"],
|
"outputs": [".next/**", "!.next/cache/**"],
|
||||||
"env": ["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY"]
|
"env": ["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY"]
|
||||||
},
|
},
|
||||||
"build:compile": {
|
|
||||||
"dependsOn": ["^build"],
|
|
||||||
"inputs": ["$TURBO_DEFAULT$"],
|
|
||||||
"outputs": ["dist/**"]
|
|
||||||
},
|
|
||||||
"start": {
|
"start": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"cache": false,
|
"cache": false,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user