Compare commits
24 Commits
main
...
ng-remove-
Author | SHA1 | Date | |
---|---|---|---|
895c04de03 | |||
f0a7cb97b4 | |||
685f8a8793 | |||
1445cb865b | |||
cd3cd7667a | |||
3a2a6dca7e | |||
ce851ef883 | |||
2c7c40697a | |||
69df4089b0 | |||
0bf6a73570 | |||
4ead8033d6 | |||
8f54da2b02 | |||
c0e3e31a81 | |||
9aa34090ba | |||
67f2ddbb53 | |||
|
96fb9eee7c | ||
|
83566f09f5 | ||
|
c816d479b4 | ||
2d51b67e58 | |||
e1a7c2bd95 | |||
|
7cb0a0048b | ||
ace6f04064 | |||
|
49add18d4e | ||
|
11c9273538 |
@ -1,76 +0,0 @@
|
||||
# backend
|
||||
|
||||
This backend is a [node.js](https://nodejs.org/) [express.js](https://expressjs.com/) [apollo server](https://www.apollographql.com/docs/apollo-server/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Install dependencies
|
||||
|
||||
In the root of the project, run:
|
||||
|
||||
```zsh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Build backend
|
||||
|
||||
```zsh
|
||||
yarn build --ignore frontend
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
#### Local
|
||||
|
||||
Copy the `environments/local.toml.example` file to `environments/local.toml`:
|
||||
|
||||
```zsh
|
||||
cp environments/local.toml.example environments/local.toml
|
||||
```
|
||||
|
||||
#### Staging environment variables
|
||||
|
||||
In the deployment repository, update staging [staging/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/staging/configmaps/config/prod.toml)
|
||||
|
||||
#### Production environment variables
|
||||
|
||||
In the deployment repository, update production [production/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/production/configmaps/config/prod.toml)
|
||||
|
||||
### Run development server
|
||||
|
||||
```zsh
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments):
|
||||
|
||||
```zsh
|
||||
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
|
||||
```
|
||||
|
||||
### Staging
|
||||
|
||||
```zsh
|
||||
echo trigger >> .gitea/workflows/triggers/staging-deploy
|
||||
git commit -a -m "Deploy v0.0.8" # replace with version number
|
||||
git push
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```zsh
|
||||
echo trigger >> .gitea/workflows/triggers/production-deploy
|
||||
git commit -a -m "Deploy v0.0.8" # replace with version number
|
||||
git push
|
||||
```
|
||||
|
||||
### Deployment status
|
||||
|
||||
Dumb for now
|
||||
|
||||
- [Staging](https://snowballtools-base-api.staging.apps.snowballtools.com/staging/version)
|
||||
- [Production](https://snowballtools-base-api.apps.snowballtools.com/staging/version)
|
||||
|
||||
Update version number manually in [routes/staging.ts](/packages/backend/src/routes/staging.ts)
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"quoteStyle": "single",
|
||||
"trailingCommas": "none",
|
||||
"semicolons": "asNeeded"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["dist/**/*", "node_modules/**/*", ".turbo/**/*"]
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 8000
|
||||
gqlPath = "/graphql"
|
||||
[server.session]
|
||||
secret = ""
|
||||
# Frontend webapp URL origin
|
||||
appOriginUrl = "http://localhost:3000"
|
||||
# Set to true if server running behind proxy
|
||||
trustProxy = false
|
||||
# Backend URL hostname
|
||||
domain = "localhost"
|
||||
|
||||
[database]
|
||||
dbPath = "db/snowball"
|
||||
|
||||
[gitHub]
|
||||
webhookUrl = ""
|
||||
[gitHub.oAuth]
|
||||
clientId = ""
|
||||
clientSecret = ""
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
checkAuctionStatusDelay = 5000
|
||||
restEndpoint = "http://localhost:1317"
|
||||
gqlEndpoint = "http://localhost:9473/api"
|
||||
chainId = "laconic_9000-1"
|
||||
privateKey = ""
|
||||
bondId = ""
|
||||
authority = ""
|
||||
[registryConfig.fee]
|
||||
gas = ""
|
||||
fees = ""
|
||||
gasPrice = "1alnt"
|
||||
|
||||
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions
|
||||
[auction]
|
||||
commitFee = "100000"
|
||||
commitsDuration = "120s"
|
||||
revealFee = "100000"
|
||||
revealsDuration = "120s"
|
||||
denom = "alnt"
|
@ -1,43 +0,0 @@
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 8000
|
||||
gqlPath = "/graphql"
|
||||
[server.session]
|
||||
secret = ""
|
||||
# Frontend webapp URL origin
|
||||
appOriginUrl = "http://localhost:3000"
|
||||
# Set to true if server running behind proxy
|
||||
trustProxy = false
|
||||
# Backend URL hostname
|
||||
domain = "localhost"
|
||||
|
||||
[database]
|
||||
dbPath = "db/snowball"
|
||||
|
||||
[gitHub]
|
||||
webhookUrl = ""
|
||||
[gitHub.oAuth]
|
||||
clientId = ""
|
||||
clientSecret = ""
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
checkAuctionStatusDelay = 5000
|
||||
restEndpoint = "http://localhost:1317"
|
||||
gqlEndpoint = "http://localhost:9473/api"
|
||||
chainId = "laconic_9000-1"
|
||||
privateKey = ""
|
||||
bondId = ""
|
||||
authority = ""
|
||||
[registryConfig.fee]
|
||||
gas = ""
|
||||
fees = ""
|
||||
gasPrice = "1alnt"
|
||||
|
||||
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions
|
||||
[auction]
|
||||
commitFee = "100000"
|
||||
commitsDuration = "120s"
|
||||
revealFee = "100000"
|
||||
revealsDuration = "120s"
|
||||
denom = "alnt"
|
@ -1,68 +0,0 @@
|
||||
{
|
||||
"name": "@qwrk/backend",
|
||||
"license": "UNLICENSED",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"@cerc-io/registry-sdk": "^0.2.11",
|
||||
"@cosmjs/stargate": "^0.33.0",
|
||||
"@graphql-tools/schema": "^10.0.2",
|
||||
"@graphql-tools/utils": "^10.0.12",
|
||||
"@octokit/oauth-app": "^6.1.0",
|
||||
"@turnkey/sdk-server": "^0.1.0",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"apollo-server-core": "^3.13.0",
|
||||
"apollo-server-express": "^3.13.0",
|
||||
"cookie-session": "^2.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-session": "^1.18.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"graphql": "^16.8.1",
|
||||
"luxon": "^3.5.0",
|
||||
"nanoid": "3",
|
||||
"nanoid-dictionary": "^5.0.0-beta.1",
|
||||
"octokit": "^3.1.2",
|
||||
"openpgp": "^6.0.1",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"semver": "^7.6.0",
|
||||
"siwe": "^3.0.0",
|
||||
"toml": "^3.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typeorm": "^0.3.19",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "DEBUG=snowball:* node --enable-source-maps ./dist/index.js",
|
||||
"start:dev": "DEBUG=snowball:* ts-node ./src/index.ts",
|
||||
"copy-assets": "copyfiles -u 1 src/**/*.gql dist/",
|
||||
"clean": "rm -rf ./dist",
|
||||
"build": "pnpm clean && tsc && pnpm copy-assets",
|
||||
"format": "biome format .",
|
||||
"format:check": "biome format --check .",
|
||||
"lint": "biome check .",
|
||||
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
|
||||
"test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts",
|
||||
"test:registry:publish-deployment-removal-records": "DEBUG=snowball:* ts-node ./test/publish-deployment-removal-records.ts",
|
||||
"test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
|
||||
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/cookie-session": "^2.0.49",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"copyfiles": "^2.4.1",
|
||||
"prettier": "^3.1.1",
|
||||
"workspace": "^0.0.1-preview.1"
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
export interface SessionConfig {
|
||||
secret: string
|
||||
appOriginUrl: string
|
||||
trustProxy: boolean
|
||||
domain: string
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
host: string
|
||||
port: number
|
||||
gqlPath?: string
|
||||
sessionSecret: string
|
||||
appOriginUrl: string
|
||||
isProduction: boolean
|
||||
session: SessionConfig
|
||||
}
|
||||
|
||||
export interface DatabaseConfig {
|
||||
dbPath: string
|
||||
}
|
||||
|
||||
export interface GitHubConfig {
|
||||
webhookUrl: string
|
||||
oAuth: {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface RegistryConfig {
|
||||
restEndpoint: string
|
||||
gqlEndpoint: string
|
||||
chainId: string
|
||||
privateKey: string
|
||||
bondId: string
|
||||
fetchDeploymentRecordDelay: number
|
||||
checkAuctionStatusDelay: number
|
||||
authority: string
|
||||
fee: {
|
||||
gas: string
|
||||
fees: string
|
||||
gasPrice: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuctionConfig {
|
||||
commitFee: string
|
||||
commitsDuration: string
|
||||
revealFee: string
|
||||
revealsDuration: string
|
||||
denom: string
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
server: ServerConfig
|
||||
database: DatabaseConfig
|
||||
gitHub: GitHubConfig
|
||||
registryConfig: RegistryConfig
|
||||
auction: AuctionConfig
|
||||
turnkey: {
|
||||
apiBaseUrl: string
|
||||
apiPublicKey: string
|
||||
apiPrivateKey: string
|
||||
defaultOrganizationId: string
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import process from 'node:process'
|
||||
|
||||
export const DEFAULT_CONFIG_FILE_PATH =
|
||||
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH ||
|
||||
'apps/backend/environments/local.toml'
|
||||
|
||||
export const DEFAULT_GQL_PATH = '/graphql'
|
@ -1,694 +0,0 @@
|
||||
import assert from 'node:assert'
|
||||
import path from 'node:path'
|
||||
import debug from 'debug'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import { lowercase, numbers } from 'nanoid-dictionary'
|
||||
import {
|
||||
DataSource,
|
||||
type DeepPartial,
|
||||
type FindManyOptions,
|
||||
type FindOneOptions,
|
||||
type FindOptionsWhere,
|
||||
IsNull,
|
||||
Not
|
||||
} from 'typeorm'
|
||||
|
||||
import type { DatabaseConfig } from './config'
|
||||
import { Deployer } from './entity/Deployer'
|
||||
import { Deployment, DeploymentStatus } from './entity/Deployment'
|
||||
import { Domain } from './entity/Domain'
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable'
|
||||
import { Organization } from './entity/Organization'
|
||||
import { Project } from './entity/Project'
|
||||
import { ProjectMember } from './entity/ProjectMember'
|
||||
import { User } from './entity/User'
|
||||
import { UserOrganization } from './entity/UserOrganization'
|
||||
import type { DNSRecordAttributes } from './types'
|
||||
import { getEntities, loadAndSaveData } from './utils'
|
||||
|
||||
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json'
|
||||
|
||||
const log = debug('snowball:database')
|
||||
|
||||
const nanoid = customAlphabet(lowercase + numbers, 8)
|
||||
|
||||
// TODO: Fix order of methods
|
||||
export class Database {
|
||||
private dataSource: DataSource
|
||||
|
||||
constructor({ dbPath }: DatabaseConfig) {
|
||||
this.dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: dbPath,
|
||||
entities: [path.join(__dirname, '/entity/*')],
|
||||
synchronize: true,
|
||||
logging: false
|
||||
})
|
||||
}
|
||||
|
||||
async init(): Promise<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
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { Column, Entity, ManyToMany, PrimaryColumn } from 'typeorm'
|
||||
import { Project } from './Project'
|
||||
|
||||
@Entity()
|
||||
export class Deployer {
|
||||
@PrimaryColumn('varchar')
|
||||
deployerLrn!: string
|
||||
|
||||
@Column('varchar')
|
||||
deployerId!: string
|
||||
|
||||
@Column('varchar')
|
||||
deployerApiUrl!: string
|
||||
|
||||
@Column('varchar')
|
||||
baseDomain!: string
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
publicKey!: string | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
minimumPayment!: string | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
paymentAddress!: string | null
|
||||
|
||||
@ManyToMany(
|
||||
() => Project,
|
||||
(project) => project.deployers
|
||||
)
|
||||
projects!: Project[]
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import type {
|
||||
AppDeploymentRecordAttributes,
|
||||
AppDeploymentRemovalRecordAttributes,
|
||||
DNSRecordAttributes
|
||||
} from '../types'
|
||||
import { Deployer } from './Deployer'
|
||||
import { Project } from './Project'
|
||||
import { User } from './User'
|
||||
|
||||
export enum Environment {
|
||||
Production = 'Production',
|
||||
Preview = 'Preview',
|
||||
Development = 'Development'
|
||||
}
|
||||
|
||||
export enum DeploymentStatus {
|
||||
Building = 'Building',
|
||||
Ready = 'Ready',
|
||||
Error = 'Error',
|
||||
Deleting = 'Deleting'
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRequest {
|
||||
type: string
|
||||
version: string
|
||||
name: string
|
||||
application: string
|
||||
lrn?: string
|
||||
auction?: string
|
||||
config: string
|
||||
meta: string
|
||||
payment?: string
|
||||
dns?: string
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRemovalRequest {
|
||||
type: string
|
||||
version: string
|
||||
deployment: string
|
||||
auction?: string
|
||||
payment?: string
|
||||
}
|
||||
|
||||
export interface ApplicationRecord {
|
||||
type: string
|
||||
version: string
|
||||
name: string
|
||||
description?: string
|
||||
homepage?: string
|
||||
license?: string
|
||||
author?: string
|
||||
repository?: string[]
|
||||
app_version?: string
|
||||
repository_ref: string
|
||||
app_type: string
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Deployment {
|
||||
// TODO: set custom generated id
|
||||
@PrimaryColumn('varchar')
|
||||
id!: string
|
||||
|
||||
@Column()
|
||||
projectId!: string
|
||||
|
||||
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'projectId' })
|
||||
project!: Project
|
||||
|
||||
@Column('varchar')
|
||||
branch!: string
|
||||
|
||||
@Column('varchar')
|
||||
commitHash!: string
|
||||
|
||||
@Column('varchar')
|
||||
commitMessage!: string
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
url!: string | null
|
||||
|
||||
@Column('varchar')
|
||||
applicationRecordId!: string
|
||||
|
||||
@Column('simple-json')
|
||||
applicationRecordData!: ApplicationRecord
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRequestId!: string | null
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRecordId!: string | null
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRequestId!: string | null
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRecordId!: string | null
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
dnsRecordData!: DNSRecordAttributes | null
|
||||
|
||||
@ManyToOne(() => Deployer)
|
||||
@JoinColumn({ name: 'deployerLrn' })
|
||||
deployer!: Deployer
|
||||
|
||||
@Column({
|
||||
enum: Environment
|
||||
})
|
||||
environment!: Environment
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isCurrent!: boolean
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isCanonical!: boolean
|
||||
|
||||
@Column({
|
||||
enum: DeploymentStatus
|
||||
})
|
||||
status!: DeploymentStatus
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'createdBy' })
|
||||
createdBy!: User
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import { Project } from './Project'
|
||||
|
||||
export enum Status {
|
||||
Live = 'Live',
|
||||
Pending = 'Pending'
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Domain {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@Column('varchar')
|
||||
projectId!: string
|
||||
|
||||
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'projectId' })
|
||||
project!: Project
|
||||
|
||||
@Column('varchar', { length: 255, default: 'main' })
|
||||
branch!: string
|
||||
|
||||
@Column('varchar', { length: 255 })
|
||||
name!: string
|
||||
|
||||
@Column('string', { nullable: true })
|
||||
redirectToId!: string | null
|
||||
|
||||
@ManyToOne(() => Domain)
|
||||
@JoinColumn({ name: 'redirectToId' })
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
redirectTo!: Domain | null
|
||||
|
||||
@Column({
|
||||
enum: Status,
|
||||
default: Status.Pending
|
||||
})
|
||||
status!: Status
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import { Project } from './Project'
|
||||
|
||||
enum Environment {
|
||||
Production = 'Production',
|
||||
Preview = 'Preview',
|
||||
Development = 'Development'
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class EnvironmentVariable {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'projectId' })
|
||||
project!: Project
|
||||
|
||||
@Column({
|
||||
enum: Environment
|
||||
})
|
||||
environment!: Environment
|
||||
|
||||
@Column('varchar')
|
||||
key!: string
|
||||
|
||||
@Column('varchar')
|
||||
value!: string
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
import { UserOrganization } from './UserOrganization'
|
||||
|
||||
@Entity()
|
||||
@Unique(['slug'])
|
||||
export class Organization {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@Column('varchar', { length: 255 })
|
||||
name!: string
|
||||
|
||||
@Column('varchar')
|
||||
slug!: string
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@OneToMany(
|
||||
() => UserOrganization,
|
||||
(userOrganization) => userOrganization.organization,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
userOrganizations!: UserOrganization[]
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import { Deployer } from './Deployer'
|
||||
import { Deployment } from './Deployment'
|
||||
import { Organization } from './Organization'
|
||||
import { ProjectMember } from './ProjectMember'
|
||||
import { User } from './User'
|
||||
|
||||
@Entity()
|
||||
export class Project {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'ownerId' })
|
||||
owner!: User
|
||||
|
||||
@Column({ nullable: false })
|
||||
ownerId!: string
|
||||
|
||||
@ManyToOne(() => Organization, { nullable: true })
|
||||
@JoinColumn({ name: 'organizationId' })
|
||||
organization!: Organization | null
|
||||
|
||||
@Column('varchar')
|
||||
organizationId!: string
|
||||
|
||||
@Column('varchar')
|
||||
name!: string
|
||||
|
||||
@Column('varchar')
|
||||
repository!: string
|
||||
|
||||
@Column('varchar', { length: 255, default: 'main' })
|
||||
prodBranch!: string
|
||||
|
||||
@Column('text', { default: '' })
|
||||
description!: string
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
auctionId!: string | null
|
||||
|
||||
// Tx hash for sending coins from snowball to deployer
|
||||
@Column('varchar', { nullable: true })
|
||||
txHash!: string | null
|
||||
|
||||
@ManyToMany(
|
||||
() => Deployer,
|
||||
(deployer) => deployer.projects
|
||||
)
|
||||
@JoinTable()
|
||||
deployers!: Deployer[]
|
||||
|
||||
@Column('boolean', { default: false, nullable: true })
|
||||
fundsReleased!: boolean
|
||||
|
||||
// TODO: Compute template & framework in import repository
|
||||
@Column('varchar', { nullable: true })
|
||||
template!: string | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
framework!: string | null
|
||||
|
||||
// Address of the user who created the project i.e. requested deployments
|
||||
@Column('varchar')
|
||||
paymentAddress!: string
|
||||
|
||||
@Column({
|
||||
type: 'simple-array'
|
||||
})
|
||||
webhooks!: string[]
|
||||
|
||||
@Column('varchar')
|
||||
icon!: string
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null
|
||||
|
||||
@OneToMany(
|
||||
() => Deployment,
|
||||
(deployment) => deployment.project
|
||||
)
|
||||
deployments!: Deployment[]
|
||||
|
||||
@OneToMany(
|
||||
() => ProjectMember,
|
||||
(projectMember) => projectMember.project,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
projectMembers!: ProjectMember[]
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import { Project } from './Project'
|
||||
import { User } from './User'
|
||||
|
||||
export enum Permission {
|
||||
View = 'View',
|
||||
Edit = 'Edit'
|
||||
}
|
||||
|
||||
@Entity()
|
||||
@Unique(['project', 'member'])
|
||||
export class ProjectMember {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@ManyToOne(
|
||||
() => User,
|
||||
(user) => user.projectMembers
|
||||
)
|
||||
@JoinColumn({ name: 'userId' })
|
||||
member!: User
|
||||
|
||||
@ManyToOne(
|
||||
() => Project,
|
||||
(project) => project.projectMembers
|
||||
)
|
||||
@JoinColumn({ name: 'projectId' })
|
||||
project!: Project
|
||||
|
||||
@Column({
|
||||
type: 'simple-array'
|
||||
})
|
||||
permissions!: Permission[]
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isPending!: boolean
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique
|
||||
} from 'typeorm'
|
||||
|
||||
import { ProjectMember } from './ProjectMember'
|
||||
import { UserOrganization } from './UserOrganization'
|
||||
|
||||
@Entity()
|
||||
@Unique(['email'])
|
||||
@Unique(['ethAddress'])
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
// TODO: Set ethAddress as ID
|
||||
@Column()
|
||||
ethAddress!: string
|
||||
|
||||
@Column('varchar', { length: 255, nullable: true })
|
||||
name!: string | null
|
||||
|
||||
@Column()
|
||||
email!: string
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
gitHubToken!: string | null
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isVerified!: boolean
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@CreateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@Column()
|
||||
subOrgId!: string
|
||||
|
||||
@Column()
|
||||
turnkeyWalletId!: string
|
||||
|
||||
@OneToMany(
|
||||
() => ProjectMember,
|
||||
(projectMember) => projectMember.project,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
projectMembers!: ProjectMember[]
|
||||
|
||||
@OneToMany(
|
||||
() => UserOrganization,
|
||||
(UserOrganization) => UserOrganization.member,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
userOrganizations!: UserOrganization[]
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import { Organization } from './Organization'
|
||||
import { User } from './User'
|
||||
|
||||
export enum Role {
|
||||
Owner = 'Owner',
|
||||
Maintainer = 'Maintainer',
|
||||
Reader = 'Reader'
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class UserOrganization {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'userId' })
|
||||
member!: User
|
||||
|
||||
@ManyToOne(() => Organization)
|
||||
@JoinColumn({ name: 'organizationId' })
|
||||
organization!: Organization
|
||||
|
||||
@Column({
|
||||
enum: Role
|
||||
})
|
||||
role!: Role
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import 'express-async-errors'
|
||||
import 'reflect-metadata'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import debug from 'debug'
|
||||
|
||||
import { OAuthApp } from '@octokit/oauth-app'
|
||||
|
||||
import { Database } from './database'
|
||||
import { Registry } from './registry'
|
||||
import { createResolvers } from './resolvers'
|
||||
import { createAndStartServer } from './server'
|
||||
import { Service } from './service'
|
||||
import { getConfig } from './utils'
|
||||
|
||||
const log = debug('snowball:server')
|
||||
const OAUTH_CLIENT_TYPE = 'oauth-app'
|
||||
|
||||
export const main = async (): Promise<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)
|
||||
})
|
@ -1,624 +0,0 @@
|
||||
import assert from 'node:assert'
|
||||
import debug from 'debug'
|
||||
import { DateTime } from 'luxon'
|
||||
import type { Octokit } from 'octokit'
|
||||
import * as openpgp from 'openpgp'
|
||||
import { inc as semverInc } from 'semver'
|
||||
import type { DeepPartial } from 'typeorm'
|
||||
|
||||
import {
|
||||
Account,
|
||||
DEFAULT_GAS_ESTIMATION_MULTIPLIER,
|
||||
Registry as LaconicRegistry,
|
||||
getGasPrice,
|
||||
parseGasAndFees
|
||||
} from '@cerc-io/registry-sdk'
|
||||
import type { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate'
|
||||
|
||||
import type { RegistryConfig } from './config'
|
||||
import type {
|
||||
ApplicationDeploymentRemovalRequest,
|
||||
ApplicationDeploymentRequest,
|
||||
ApplicationRecord,
|
||||
Deployment
|
||||
} from './entity/Deployment'
|
||||
import type {
|
||||
AppDeploymentRecord,
|
||||
AppDeploymentRemovalRecord,
|
||||
AuctionParams,
|
||||
DeployerRecord,
|
||||
RegistryRecord
|
||||
} from './types'
|
||||
import {
|
||||
getConfig,
|
||||
getRepoDetails,
|
||||
registryTransactionWithRetry,
|
||||
sleep
|
||||
} from './utils'
|
||||
|
||||
const log = debug('snowball:registry')
|
||||
|
||||
const APP_RECORD_TYPE = 'ApplicationRecord'
|
||||
const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction'
|
||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest'
|
||||
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE =
|
||||
'ApplicationDeploymentRemovalRequest'
|
||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'
|
||||
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord'
|
||||
const WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer'
|
||||
const SLEEP_DURATION = 1000
|
||||
|
||||
// TODO: Move registry code to registry-sdk/watcher-ts
|
||||
export class Registry {
|
||||
private registry: LaconicRegistry
|
||||
private registryConfig: RegistryConfig
|
||||
|
||||
constructor(registryConfig: RegistryConfig) {
|
||||
this.registryConfig = registryConfig
|
||||
|
||||
const gasPrice = getGasPrice(registryConfig.fee.gasPrice)
|
||||
|
||||
this.registry = new LaconicRegistry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId, gasPrice }
|
||||
)
|
||||
}
|
||||
|
||||
async createApplicationRecord({
|
||||
octokit,
|
||||
repository,
|
||||
commitHash,
|
||||
appType
|
||||
}: {
|
||||
octokit: Octokit
|
||||
repository: string
|
||||
commitHash: string
|
||||
appType: string
|
||||
}): Promise<{
|
||||
applicationRecordId: string
|
||||
applicationRecordData: ApplicationRecord
|
||||
}> {
|
||||
const { repo, repoUrl, packageJSON } = await getRepoDetails(
|
||||
octokit,
|
||||
repository,
|
||||
commitHash
|
||||
)
|
||||
// Use registry-sdk to publish record
|
||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||
// Fetch previous records
|
||||
const records = await this.registry.queryRecords(
|
||||
{
|
||||
type: APP_RECORD_TYPE,
|
||||
name: packageJSON.name
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
// Get next version of record
|
||||
const bondRecords = records.filter(
|
||||
(record: any) => record.bondId === this.registryConfig.bondId
|
||||
)
|
||||
const [latestBondRecord] = bondRecords.sort(
|
||||
(a: any, b: any) =>
|
||||
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||
)
|
||||
const nextVersion = semverInc(
|
||||
latestBondRecord?.attributes.version ?? '0.0.0',
|
||||
'patch'
|
||||
)
|
||||
|
||||
assert(nextVersion, 'Application record version not valid')
|
||||
|
||||
// Create record of type ApplicationRecord and publish
|
||||
const applicationRecord = {
|
||||
type: APP_RECORD_TYPE,
|
||||
version: nextVersion,
|
||||
repository_ref: commitHash,
|
||||
repository: [repoUrl],
|
||||
app_type: appType,
|
||||
name: repo,
|
||||
...(packageJSON.description && { description: packageJSON.description }),
|
||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||
...(packageJSON.license && { license: packageJSON.license }),
|
||||
...(packageJSON.author && {
|
||||
author:
|
||||
typeof packageJSON.author === 'object'
|
||||
? JSON.stringify(packageJSON.author)
|
||||
: packageJSON.author
|
||||
}),
|
||||
...(packageJSON.version && { app_version: packageJSON.version })
|
||||
}
|
||||
|
||||
const result = await this.publishRecord(applicationRecord)
|
||||
|
||||
log(`Published application record ${result.id}`)
|
||||
log('Application record data:', applicationRecord)
|
||||
|
||||
// TODO: Discuss computation of LRN
|
||||
const lrn = this.getLrn(repo)
|
||||
log(`Setting name: ${lrn} for record ID: ${result.id}`)
|
||||
|
||||
const fee = parseGasAndFees(
|
||||
this.registryConfig.fee.gas,
|
||||
this.registryConfig.fee.fees
|
||||
)
|
||||
|
||||
await sleep(SLEEP_DURATION)
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
)
|
||||
|
||||
await sleep(SLEEP_DURATION)
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn: `${lrn}@${applicationRecord.app_version}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
)
|
||||
|
||||
await sleep(SLEEP_DURATION)
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn: `${lrn}@${applicationRecord.repository_ref}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
applicationRecordId: result.id,
|
||||
applicationRecordData: applicationRecord
|
||||
}
|
||||
}
|
||||
|
||||
async createApplicationDeploymentAuction(
|
||||
appName: string,
|
||||
octokit: Octokit,
|
||||
auctionParams: AuctionParams,
|
||||
data: DeepPartial<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
|
||||
}
|
||||
}
|
@ -1,413 +0,0 @@
|
||||
import debug from 'debug'
|
||||
import type { DeepPartial, FindOptionsWhere } from 'typeorm'
|
||||
|
||||
import type { Domain } from './entity/Domain'
|
||||
import type { EnvironmentVariable } from './entity/EnvironmentVariable'
|
||||
import type { Project } from './entity/Project'
|
||||
import type { Permission } from './entity/ProjectMember'
|
||||
import type { Service } from './service'
|
||||
import type {
|
||||
AddProjectFromTemplateInput,
|
||||
AuctionParams,
|
||||
EnvironmentVariables
|
||||
} from './types'
|
||||
|
||||
const log = debug('snowball:resolver')
|
||||
|
||||
export const createResolvers = async (service: Service): Promise<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
import { SiweMessage } from 'siwe'
|
||||
import type { Service } from '../service'
|
||||
import { authenticateUser, createUser } from '../turnkey-backend'
|
||||
|
||||
const router: Router = Router()
|
||||
|
||||
//
|
||||
// Turnkey
|
||||
//
|
||||
router.get('/registration/:email', async (req, res) => {
|
||||
const service: Service = req.app.get('service')
|
||||
const user = await service.getUserByEmail(req.params.email)
|
||||
if (user) {
|
||||
return res.send({ subOrganizationId: user?.subOrgId })
|
||||
}
|
||||
|
||||
return res.sendStatus(204)
|
||||
})
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
console.log('Register', req.body)
|
||||
const { email, challenge, attestation } = req.body
|
||||
const user = await createUser(req.app.get('service'), {
|
||||
challenge,
|
||||
attestation,
|
||||
userEmail: email,
|
||||
userName: email.split('@')[0]
|
||||
})
|
||||
req.session.address = user.id
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
router.post('/authenticate', async (req, res) => {
|
||||
console.log('Authenticate', req.body)
|
||||
const { signedWhoamiRequest } = req.body
|
||||
const user = await authenticateUser(
|
||||
req.app.get('service'),
|
||||
signedWhoamiRequest
|
||||
)
|
||||
if (user) {
|
||||
req.session.address = user.id
|
||||
res.sendStatus(200)
|
||||
} else {
|
||||
res.sendStatus(401)
|
||||
}
|
||||
})
|
||||
|
||||
//
|
||||
// SIWE Auth
|
||||
//
|
||||
router.post('/validate', async (req, res) => {
|
||||
const { message, signature } = req.body
|
||||
const { success, data } = await new SiweMessage(message).verify({
|
||||
signature
|
||||
})
|
||||
|
||||
if (!success) {
|
||||
return res.send({ success })
|
||||
}
|
||||
const service: Service = req.app.get('service')
|
||||
const user = await service.getUserByEthAddress(data.address)
|
||||
|
||||
if (!user) {
|
||||
const newUser = await service.createUser({
|
||||
ethAddress: data.address,
|
||||
email: `${data.address}@example.com`,
|
||||
subOrgId: '',
|
||||
turnkeyWalletId: ''
|
||||
})
|
||||
|
||||
// SIWESession from the web3modal library requires both address and chain ID
|
||||
req.session.address = newUser.id
|
||||
req.session.chainId = data.chainId
|
||||
} else {
|
||||
req.session.address = user.id
|
||||
req.session.chainId = data.chainId
|
||||
}
|
||||
|
||||
res.send({ success })
|
||||
})
|
||||
|
||||
//
|
||||
// General
|
||||
//
|
||||
router.get('/session', (req, res) => {
|
||||
if (req.session.address && req.session.chainId) {
|
||||
res.send({
|
||||
address: req.session.address,
|
||||
chainId: req.session.chainId
|
||||
})
|
||||
} else {
|
||||
res.status(401).send({ error: 'Unauthorized: No active session' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
@ -1,26 +0,0 @@
|
||||
import debug from 'debug'
|
||||
import { Router } from 'express'
|
||||
|
||||
import type { Service } from '../service'
|
||||
|
||||
const log = debug('snowball:routes-github')
|
||||
const router: Router = Router()
|
||||
|
||||
/* POST GitHub webhook handler */
|
||||
// https://docs.github.com/en/webhooks/using-webhooks/handling-webhook-deliveries#javascript-example
|
||||
router.post('/webhook', async (req, res) => {
|
||||
// Server should respond with a 2XX response within 10 seconds of receiving a webhook delivery
|
||||
// If server takes longer than that to respond, then GitHub terminates the connection and considers the delivery a failure
|
||||
res.status(202).send('Accepted')
|
||||
|
||||
const service = req.app.get('service') as Service
|
||||
const githubEvent = req.headers['x-github-event']
|
||||
log(`Received GitHub webhook for event ${githubEvent}`)
|
||||
|
||||
if (githubEvent === 'push') {
|
||||
// Create deployments using push event data
|
||||
await service.handleGitHubPush(req.body)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
@ -1,9 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
const router: Router = Router()
|
||||
|
||||
router.get('/version', async (_req, res) => {
|
||||
return res.send({ version: '0.0.9' })
|
||||
})
|
||||
|
||||
export default router
|
@ -1,337 +0,0 @@
|
||||
enum Role {
|
||||
Owner
|
||||
Maintainer
|
||||
Reader
|
||||
}
|
||||
|
||||
enum Permission {
|
||||
View
|
||||
Edit
|
||||
}
|
||||
|
||||
enum Environment {
|
||||
Production
|
||||
Preview
|
||||
Development
|
||||
}
|
||||
|
||||
enum DeploymentStatus {
|
||||
Building
|
||||
Ready
|
||||
Error
|
||||
Deleting
|
||||
}
|
||||
|
||||
enum AuctionStatus {
|
||||
completed
|
||||
reveal
|
||||
commit
|
||||
expired
|
||||
}
|
||||
|
||||
enum DomainStatus {
|
||||
Live
|
||||
Pending
|
||||
}
|
||||
|
||||
type User {
|
||||
id: String!
|
||||
name: String
|
||||
email: String!
|
||||
organizations: [Organization!]
|
||||
projects: [Project!]
|
||||
isVerified: Boolean!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
gitHubToken: String
|
||||
}
|
||||
|
||||
type Organization {
|
||||
id: String!
|
||||
name: String!
|
||||
slug: String!
|
||||
projects: [Project!]
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
members: [OrganizationMember!]
|
||||
}
|
||||
|
||||
type OrganizationMember {
|
||||
id: String!
|
||||
member: User!
|
||||
role: Role!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Project {
|
||||
id: String!
|
||||
owner: User!
|
||||
deployments: [Deployment!]
|
||||
name: String!
|
||||
repository: String!
|
||||
prodBranch: String!
|
||||
description: String
|
||||
deployers: [Deployer!]
|
||||
auctionId: String
|
||||
fundsReleased: Boolean
|
||||
template: String
|
||||
framework: String
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
webhooks: [String!]
|
||||
members: [ProjectMember!]
|
||||
environmentVariables: [EnvironmentVariable!]
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
organization: Organization!
|
||||
icon: String
|
||||
baseDomains: [String!]
|
||||
}
|
||||
|
||||
type ProjectMember {
|
||||
id: String!
|
||||
member: User!
|
||||
permissions: [Permission!]!
|
||||
isPending: Boolean!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Deployment {
|
||||
id: String!
|
||||
branch: String!
|
||||
commitHash: String!
|
||||
commitMessage: String!
|
||||
url: String
|
||||
environment: Environment!
|
||||
deployer: Deployer
|
||||
applicationDeploymentRequestId: String
|
||||
applicationDeploymentRecordData: AppDeploymentRecordAttributes
|
||||
isCurrent: Boolean!
|
||||
baseDomain: String
|
||||
status: DeploymentStatus!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
createdBy: User!
|
||||
}
|
||||
|
||||
type Domain {
|
||||
id: String!
|
||||
branch: String!
|
||||
name: String!
|
||||
redirectTo: Domain
|
||||
status: DomainStatus!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type EnvironmentVariable {
|
||||
id: String!
|
||||
environment: Environment!
|
||||
key: String!
|
||||
value: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Deployer {
|
||||
deployerLrn: String!
|
||||
deployerId: String!
|
||||
deployerApiUrl: String!
|
||||
minimumPayment: String
|
||||
paymentAddress: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
baseDomain: String
|
||||
}
|
||||
|
||||
type AuthResult {
|
||||
token: String!
|
||||
}
|
||||
|
||||
input AddEnvironmentVariableInput {
|
||||
environments: [Environment!]!
|
||||
key: String!
|
||||
value: String!
|
||||
}
|
||||
|
||||
input AddProjectFromTemplateInput {
|
||||
templateOwner: String!
|
||||
templateRepo: String!
|
||||
owner: String!
|
||||
name: String!
|
||||
isPrivate: Boolean!
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
}
|
||||
|
||||
input AddProjectInput {
|
||||
name: String!
|
||||
repository: String!
|
||||
prodBranch: String!
|
||||
template: String
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
}
|
||||
|
||||
input UpdateProjectInput {
|
||||
name: String
|
||||
description: String
|
||||
prodBranch: String
|
||||
organizationId: String
|
||||
webhooks: [String!]
|
||||
}
|
||||
|
||||
input AddDomainInput {
|
||||
name: String!
|
||||
}
|
||||
|
||||
input UpdateDomainInput {
|
||||
name: String
|
||||
branch: String
|
||||
redirectToId: String
|
||||
}
|
||||
|
||||
input UpdateEnvironmentVariableInput {
|
||||
key: String
|
||||
value: String
|
||||
}
|
||||
|
||||
input AddProjectMemberInput {
|
||||
email: String!
|
||||
permissions: [Permission!]
|
||||
}
|
||||
|
||||
input UpdateProjectMemberInput {
|
||||
permissions: [Permission]
|
||||
}
|
||||
|
||||
input FilterDomainsInput {
|
||||
branch: String
|
||||
status: DomainStatus
|
||||
}
|
||||
|
||||
type Fee {
|
||||
type: String!
|
||||
quantity: String!
|
||||
}
|
||||
|
||||
type Bid {
|
||||
auctionId: String!
|
||||
bidderAddress: String!
|
||||
status: String!
|
||||
commitHash: String!
|
||||
commitTime: String
|
||||
commitFee: Fee
|
||||
revealTime: String
|
||||
revealFee: Fee
|
||||
bidAmount: Fee
|
||||
}
|
||||
|
||||
type Auction {
|
||||
id: String!
|
||||
kind: String!
|
||||
status: String!
|
||||
ownerAddress: String!
|
||||
createTime: String!
|
||||
commitsEndTime: String!
|
||||
revealsEndTime: String!
|
||||
commitFee: Fee!
|
||||
revealFee: Fee!
|
||||
minimumBid: Fee
|
||||
winnerAddresses: [String!]!
|
||||
winnerBids: [Fee!]
|
||||
winnerPrice: Fee
|
||||
maxPrice: Fee
|
||||
numProviders: Int!
|
||||
fundsReleased: Boolean!
|
||||
bids: [Bid!]!
|
||||
}
|
||||
|
||||
type DNSRecordAttributes {
|
||||
name: String
|
||||
value: String
|
||||
request: String
|
||||
resourceType: String
|
||||
version: String
|
||||
}
|
||||
|
||||
type AppDeploymentRecordAttributes {
|
||||
application: String
|
||||
auction: String
|
||||
deployer: String
|
||||
dns: String
|
||||
meta: String
|
||||
name: String
|
||||
request: String
|
||||
type: String
|
||||
url: String
|
||||
version: String
|
||||
}
|
||||
|
||||
input AuctionParams {
|
||||
maxPrice: String
|
||||
numProviders: Int
|
||||
}
|
||||
|
||||
type Query {
|
||||
user: User!
|
||||
organizations: [Organization!]
|
||||
projects: [Project!]
|
||||
projectsInOrganization(organizationSlug: String!): [Project!]
|
||||
project(projectId: String!): Project
|
||||
deployments(projectId: String!): [Deployment!]
|
||||
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
||||
projectMembers(projectId: String!): [ProjectMember!]
|
||||
searchProjects(searchText: String!): [Project!]
|
||||
getAuctionData(auctionId: String!): Auction!
|
||||
latestDNSRecord(projectId: String!): DNSRecordAttributes
|
||||
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
||||
deployers: [Deployer]
|
||||
address: String!
|
||||
verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
||||
updateProjectMember(
|
||||
projectMemberId: String!
|
||||
data: UpdateProjectMemberInput
|
||||
): Boolean!
|
||||
removeProjectMember(projectMemberId: String!): Boolean!
|
||||
addEnvironmentVariables(
|
||||
projectId: String!
|
||||
data: [AddEnvironmentVariableInput!]
|
||||
): Boolean!
|
||||
updateEnvironmentVariable(
|
||||
environmentVariableId: String!
|
||||
data: UpdateEnvironmentVariableInput!
|
||||
): Boolean!
|
||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||
addProjectFromTemplate(
|
||||
organizationSlug: String!
|
||||
data: AddProjectFromTemplateInput
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: [AddEnvironmentVariableInput!]
|
||||
): Project!
|
||||
addProject(
|
||||
organizationSlug: String!
|
||||
data: AddProjectInput!
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: [AddEnvironmentVariableInput!]
|
||||
): Project!
|
||||
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
||||
redeployToProd(deploymentId: String!): Boolean!
|
||||
deleteProject(projectId: String!): Boolean!
|
||||
deleteDomain(domainId: String!): Boolean!
|
||||
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
|
||||
deleteDeployment(deploymentId: String!): Boolean!
|
||||
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
||||
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
||||
authenticateGitHub(code: String!): AuthResult!
|
||||
unauthenticateGitHub: Boolean!
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
import {
|
||||
ApolloServerPluginDrainHttpServer,
|
||||
ApolloServerPluginLandingPageLocalDefault,
|
||||
AuthenticationError
|
||||
} from 'apollo-server-core'
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import cors from 'cors'
|
||||
import debug from 'debug'
|
||||
import express from 'express'
|
||||
import session from 'express-session'
|
||||
import { createServer } from 'node:http'
|
||||
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema'
|
||||
import type { TypeSource } from '@graphql-tools/utils'
|
||||
|
||||
import type { ServerConfig } from './config'
|
||||
import authRouter from './routes/auth'
|
||||
import githubRouter from './routes/github'
|
||||
import stagingRouter from './routes/staging'
|
||||
import type { Service } from './service'
|
||||
|
||||
const log = debug('snowball:server')
|
||||
|
||||
// Set cookie expiration to 1 month in milliseconds
|
||||
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60 * 1000
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
address: string
|
||||
chainId: number
|
||||
}
|
||||
}
|
||||
|
||||
export const createAndStartServer = async (
|
||||
serverConfig: ServerConfig,
|
||||
typeDefs: TypeSource,
|
||||
resolvers: any,
|
||||
service: Service
|
||||
): Promise<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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,130 +0,0 @@
|
||||
import { Turnkey, type TurnkeyApiTypes } from '@turnkey/sdk-server'
|
||||
|
||||
// Default path for the first Ethereum address in a new HD wallet.
|
||||
// See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki, paths are in the form:
|
||||
// m / purpose' / coin_type' / account' / change / address_index
|
||||
// - Purpose is a constant set to 44' following the BIP43 recommendation.
|
||||
// - Coin type is set to 60 (ETH) -- see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
// - Account, Change, and Address Index are set to 0
|
||||
import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-server'
|
||||
import type { Service } from './service'
|
||||
import { getConfig } from './utils'
|
||||
|
||||
type TAttestation = TurnkeyApiTypes['v1Attestation']
|
||||
|
||||
type CreateUserParams = {
|
||||
userName: string
|
||||
userEmail: string
|
||||
challenge: string
|
||||
attestation: TAttestation
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
service: Service,
|
||||
{ userName, userEmail, challenge, attestation }: CreateUserParams
|
||||
) {
|
||||
try {
|
||||
if (await service.getUserByEmail(userEmail)) {
|
||||
throw new Error(`User already exists: ${userEmail}`)
|
||||
}
|
||||
|
||||
const config = await getConfig()
|
||||
const turnkey = new Turnkey(config.turnkey)
|
||||
|
||||
const apiClient = turnkey.api()
|
||||
|
||||
const walletName = 'Default ETH Wallet'
|
||||
|
||||
const createSubOrgResponse = await apiClient.createSubOrganization({
|
||||
subOrganizationName: `Default SubOrg for ${userEmail}`,
|
||||
rootQuorumThreshold: 1,
|
||||
rootUsers: [
|
||||
{
|
||||
userName,
|
||||
userEmail,
|
||||
apiKeys: [],
|
||||
authenticators: [
|
||||
{
|
||||
authenticatorName: 'Passkey',
|
||||
challenge,
|
||||
attestation
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
wallet: {
|
||||
walletName: walletName,
|
||||
accounts: DEFAULT_ETHEREUM_ACCOUNTS
|
||||
}
|
||||
})
|
||||
|
||||
const subOrgId = refineNonNull(createSubOrgResponse.subOrganizationId)
|
||||
const wallet = refineNonNull(createSubOrgResponse.wallet)
|
||||
|
||||
const result = {
|
||||
id: wallet.walletId,
|
||||
address: wallet.addresses[0],
|
||||
subOrgId: subOrgId
|
||||
}
|
||||
console.log('Turnkey success', result)
|
||||
|
||||
const user = await service.createUser({
|
||||
name: userName,
|
||||
email: userEmail,
|
||||
subOrgId,
|
||||
ethAddress: wallet.addresses[0],
|
||||
turnkeyWalletId: wallet.walletId
|
||||
})
|
||||
console.log('New user', user)
|
||||
|
||||
return user
|
||||
} catch (e) {
|
||||
console.error('Failed to create user:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function authenticateUser(
|
||||
service: Service,
|
||||
signedWhoamiRequest: {
|
||||
url: string
|
||||
body: any
|
||||
stamp: {
|
||||
stampHeaderName: string
|
||||
stampHeaderValue: string
|
||||
}
|
||||
}
|
||||
) {
|
||||
try {
|
||||
const tkRes = await fetch(signedWhoamiRequest.url, {
|
||||
method: 'POST',
|
||||
body: signedWhoamiRequest.body,
|
||||
headers: {
|
||||
[signedWhoamiRequest.stamp.stampHeaderName]:
|
||||
signedWhoamiRequest.stamp.stampHeaderValue
|
||||
}
|
||||
})
|
||||
console.log('AUTH RESULT', tkRes.status)
|
||||
if (tkRes.status !== 200) {
|
||||
console.log(await tkRes.text())
|
||||
return null
|
||||
}
|
||||
const orgId = (await tkRes.json()).organizationId
|
||||
const user = await service.getUserBySubOrgId(orgId)
|
||||
return user
|
||||
} catch (e) {
|
||||
console.error('Failed to authenticate:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function refineNonNull<T>(
|
||||
input: T | null | undefined,
|
||||
errorMessage?: string
|
||||
): T {
|
||||
if (input == null) {
|
||||
throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`)
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
export interface PackageJSON {
|
||||
name: string
|
||||
version: string
|
||||
author?: string
|
||||
description?: string
|
||||
homepage?: string
|
||||
license?: string
|
||||
repository?: string
|
||||
}
|
||||
|
||||
export interface GitRepositoryDetails {
|
||||
id: number
|
||||
name: string
|
||||
full_name: string
|
||||
visibility?: string
|
||||
updated_at?: string | null
|
||||
default_branch?: string
|
||||
}
|
||||
|
||||
export interface GitPushEventPayload {
|
||||
repository: GitRepositoryDetails
|
||||
ref: string
|
||||
head_commit: {
|
||||
id: string
|
||||
message: string
|
||||
}
|
||||
deleted: boolean
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecordAttributes {
|
||||
application: string
|
||||
auction: string
|
||||
deployer: string
|
||||
dns: string
|
||||
meta: string
|
||||
name: string
|
||||
request: string
|
||||
type: string
|
||||
url: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface DNSRecordAttributes {
|
||||
name: string
|
||||
value: string
|
||||
request: string
|
||||
resourceType: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface RegistryDNSRecordAttributes {
|
||||
name: string
|
||||
value: string
|
||||
request: string
|
||||
resource_type: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface AppDeploymentRemovalRecordAttributes {
|
||||
deployment: string
|
||||
request: string
|
||||
type: 'ApplicationDeploymentRemovalRecord'
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface RegistryRecord {
|
||||
id: string
|
||||
names: string[] | null
|
||||
owners: string[]
|
||||
bondId: string
|
||||
createTime: string
|
||||
expiryTime: string
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecord extends RegistryRecord {
|
||||
attributes: AppDeploymentRecordAttributes
|
||||
}
|
||||
|
||||
export interface AppDeploymentRemovalRecord extends RegistryRecord {
|
||||
attributes: AppDeploymentRemovalRecordAttributes
|
||||
}
|
||||
|
||||
export interface DNSRecord extends RegistryRecord {
|
||||
attributes: RegistryDNSRecordAttributes
|
||||
}
|
||||
|
||||
export interface AddProjectFromTemplateInput {
|
||||
templateOwner: string
|
||||
templateRepo: string
|
||||
owner: string
|
||||
name: string
|
||||
isPrivate: boolean
|
||||
paymentAddress: string
|
||||
txHash: string
|
||||
}
|
||||
|
||||
export interface AuctionParams {
|
||||
maxPrice: string
|
||||
numProviders: number
|
||||
}
|
||||
|
||||
export interface EnvironmentVariables {
|
||||
environments: string[]
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface DeployerRecord {
|
||||
id: string
|
||||
names: string[]
|
||||
owners: string[]
|
||||
bondId: string
|
||||
createTime: string
|
||||
expiryTime: string
|
||||
attributes: {
|
||||
apiUrl: string
|
||||
minimumPayment: string | null
|
||||
name: string
|
||||
paymentAddress: string
|
||||
publicKey: string
|
||||
type: string
|
||||
version: string
|
||||
}
|
||||
}
|
@ -1,160 +0,0 @@
|
||||
import debug from 'debug'
|
||||
import fs from 'fs-extra'
|
||||
import assert from 'node:assert'
|
||||
import path from 'node:path'
|
||||
import type { Octokit } from 'octokit'
|
||||
import toml from 'toml'
|
||||
import type {
|
||||
DataSource,
|
||||
DeepPartial,
|
||||
EntityTarget,
|
||||
ObjectLiteral
|
||||
} from 'typeorm'
|
||||
|
||||
import type { Config } from './config'
|
||||
|
||||
interface PackageJSON {
|
||||
name: string
|
||||
description?: string
|
||||
homepage?: string
|
||||
license?: string
|
||||
author?: string | { [key: string]: unknown }
|
||||
version?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const log = debug('snowball:utils')
|
||||
|
||||
export async function getConfig() {
|
||||
return await _getConfig<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}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import * as fs from 'node:fs/promises'
|
||||
import debug from 'debug'
|
||||
|
||||
import { getConfig } from '../src/utils'
|
||||
|
||||
const log = debug('snowball:delete-database')
|
||||
|
||||
const deleteFile = async (filePath: string) => {
|
||||
await fs.unlink(filePath)
|
||||
log(`File ${filePath} has been deleted.`)
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
const config = await getConfig()
|
||||
|
||||
deleteFile(config.database.dbPath)
|
||||
}
|
||||
|
||||
main().catch((err) => log(err))
|
189
apps/backend/test/fixtures/deployments.json
vendored
189
apps/backend/test/fixtures/deployments.json
vendored
@ -1,189 +0,0 @@
|
||||
[
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex": 0,
|
||||
"createdByIndex": 0,
|
||||
"id": "ffhae3zq",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "main",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-ffhae3zq.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex": 1,
|
||||
"createdByIndex": 0,
|
||||
"id": "vehagei8",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-vehagei8.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex": 2,
|
||||
"createdByIndex": 0,
|
||||
"id": "qmgekyte",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-qmgekyte.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex": null,
|
||||
"createdByIndex": 0,
|
||||
"id": "f8wsyim6",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "prod",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-f8wsyim6.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex": 3,
|
||||
"createdByIndex": 1,
|
||||
"id": "eO8cckxk",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "main",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-2-eO8cckxk.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex": 4,
|
||||
"createdByIndex": 1,
|
||||
"id": "yaq0t5yw",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-2-yaq0t5yw.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex": 5,
|
||||
"createdByIndex": 1,
|
||||
"id": "hwwr6sbx",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-2-hwwr6sbx.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex": 9,
|
||||
"createdByIndex": 2,
|
||||
"id": "ndxje48a",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "main",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "iglootools-ndxje48a.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex": 7,
|
||||
"createdByIndex": 2,
|
||||
"id": "gtgpgvei",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "iglootools-gtgpgvei.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex": 8,
|
||||
"createdByIndex": 2,
|
||||
"id": "b4bpthjr",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "iglootools-b4bpthjr.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 3,
|
||||
"domainIndex": 6,
|
||||
"createdByIndex": 2,
|
||||
"id": "b4bpthjr",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "iglootools-b4bpthjr.snowball.xyz"
|
||||
}
|
||||
]
|
@ -1,92 +0,0 @@
|
||||
[
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Production"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"key": "XYZ",
|
||||
"value": "abc3",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Production"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"key": "XYZ",
|
||||
"value": "abc3",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Production"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"key": "XYZ",
|
||||
"value": "abc3",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 3,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Production"
|
||||
},
|
||||
{
|
||||
"projectIndex": 3,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 3,
|
||||
"key": "XYZ",
|
||||
"value": "abc3",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 4,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Production"
|
||||
},
|
||||
{
|
||||
"projectIndex": 4,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 4,
|
||||
"key": "XYZ",
|
||||
"value": "abc3",
|
||||
"environment": "Preview"
|
||||
}
|
||||
]
|
@ -1,7 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
||||
"name": "Deploy Tools",
|
||||
"slug": "deploy-tools"
|
||||
}
|
||||
]
|
44
apps/backend/test/fixtures/primary-domains.json
vendored
44
apps/backend/test/fixtures/primary-domains.json
vendored
@ -1,44 +0,0 @@
|
||||
[
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"name": "example.snowballtools.xyz",
|
||||
"status": "Live",
|
||||
"branch": "main"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"name": "example.org",
|
||||
"status": "Pending",
|
||||
"branch": "test"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"name": "example.snowballtools.xyz",
|
||||
"status": "Live",
|
||||
"branch": "main"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"name": "example.org",
|
||||
"status": "Pending",
|
||||
"branch": "test"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"name": "example.snowballtools.xyz",
|
||||
"status": "Live",
|
||||
"branch": "main"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"name": "example.org",
|
||||
"status": "Pending",
|
||||
"branch": "test"
|
||||
},
|
||||
{
|
||||
"projectIndex": 3,
|
||||
"name": "iglootools-2.com",
|
||||
"status": "Pending",
|
||||
"branch": "test"
|
||||
}
|
||||
]
|
56
apps/backend/test/fixtures/project-members.json
vendored
56
apps/backend/test/fixtures/project-members.json
vendored
@ -1,56 +0,0 @@
|
||||
[
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 0,
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 0,
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 1,
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 2,
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 2,
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 3,
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 3,
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 4,
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 4,
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
}
|
||||
]
|
67
apps/backend/test/fixtures/projects.json
vendored
67
apps/backend/test/fixtures/projects.json
vendored
@ -1,67 +0,0 @@
|
||||
[
|
||||
{
|
||||
"ownerIndex": 0,
|
||||
"organizationIndex": 0,
|
||||
"name": "testProject",
|
||||
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||
"prodBranch": "main",
|
||||
"description": "test",
|
||||
"template": "webapp",
|
||||
"framework": "test",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"subDomain": "testProject.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"ownerIndex": 1,
|
||||
"organizationIndex": 0,
|
||||
"name": "testProject-2",
|
||||
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||
"prodBranch": "main",
|
||||
"description": "test-2",
|
||||
"template": "webapp",
|
||||
"framework": "test-2",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"subDomain": "testProject-2.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"ownerIndex": 2,
|
||||
"organizationIndex": 0,
|
||||
"name": "iglootools",
|
||||
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||
"prodBranch": "main",
|
||||
"description": "test-3",
|
||||
"template": "webapp",
|
||||
"framework": "test-3",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"subDomain": "iglootools.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"ownerIndex": 1,
|
||||
"organizationIndex": 0,
|
||||
"name": "iglootools-2",
|
||||
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||
"prodBranch": "main",
|
||||
"description": "test-4",
|
||||
"template": "webapp",
|
||||
"framework": "test-4",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"subDomain": "iglootools-2.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"ownerIndex": 0,
|
||||
"organizationIndex": 1,
|
||||
"name": "snowball-2",
|
||||
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||
"prodBranch": "main",
|
||||
"description": "test-5",
|
||||
"template": "webapp",
|
||||
"framework": "test-5",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"subDomain": "snowball-2.snowball.xyz"
|
||||
}
|
||||
]
|
@ -1,23 +0,0 @@
|
||||
[
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"name": "www.example.org",
|
||||
"status": "Pending",
|
||||
"redirectToIndex": 1,
|
||||
"branch": "test"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"name": "www.example.org",
|
||||
"status": "Pending",
|
||||
"redirectToIndex": 3,
|
||||
"branch": "test"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"name": "www.example.org",
|
||||
"status": "Pending",
|
||||
"redirectToIndex": 5,
|
||||
"branch": "test"
|
||||
}
|
||||
]
|
@ -1,22 +0,0 @@
|
||||
[
|
||||
{
|
||||
"role": "Owner",
|
||||
"memberIndex": 0,
|
||||
"organizationIndex": 0
|
||||
},
|
||||
{
|
||||
"role": "Maintainer",
|
||||
"memberIndex": 1,
|
||||
"organizationIndex": 0
|
||||
},
|
||||
{
|
||||
"role": "Owner",
|
||||
"memberIndex": 2,
|
||||
"organizationIndex": 0
|
||||
},
|
||||
{
|
||||
"role": "Owner",
|
||||
"memberIndex": 0,
|
||||
"organizationIndex": 1
|
||||
}
|
||||
]
|
23
apps/backend/test/fixtures/users.json
vendored
23
apps/backend/test/fixtures/users.json
vendored
@ -1,23 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
|
||||
"name": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
||||
"email": "snowball@snowballtools.xyz",
|
||||
"isVerified": true,
|
||||
"ethAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||
},
|
||||
{
|
||||
"id": "e505b212-8da6-48b2-9614-098225dab34b",
|
||||
"name": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
|
||||
"email": "alice@snowballtools.xyz",
|
||||
"isVerified": true,
|
||||
"ethAddress": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"
|
||||
},
|
||||
{
|
||||
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
|
||||
"name": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a",
|
||||
"email": "bob@snowballtools.xyz",
|
||||
"isVerified": true,
|
||||
"ethAddress": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a"
|
||||
}
|
||||
]
|
@ -1,176 +0,0 @@
|
||||
import path from 'node:path'
|
||||
import debug from 'debug'
|
||||
import { DataSource } from 'typeorm'
|
||||
|
||||
import { Deployment } from '../src/entity/Deployment'
|
||||
import { Domain } from '../src/entity/Domain'
|
||||
import { EnvironmentVariable } from '../src/entity/EnvironmentVariable'
|
||||
import { Organization } from '../src/entity/Organization'
|
||||
import { Project } from '../src/entity/Project'
|
||||
import { ProjectMember } from '../src/entity/ProjectMember'
|
||||
import { User } from '../src/entity/User'
|
||||
import { UserOrganization } from '../src/entity/UserOrganization'
|
||||
import {
|
||||
checkFileExists,
|
||||
getConfig,
|
||||
getEntities,
|
||||
loadAndSaveData
|
||||
} from '../src/utils'
|
||||
|
||||
const log = debug('snowball:initialize-database')
|
||||
|
||||
const USER_DATA_PATH = './fixtures/users.json'
|
||||
const PROJECT_DATA_PATH = './fixtures/projects.json'
|
||||
const ORGANIZATION_DATA_PATH = './fixtures/organizations.json'
|
||||
const USER_ORGANIZATION_DATA_PATH = './fixtures/user-organizations.json'
|
||||
const PROJECT_MEMBER_DATA_PATH = './fixtures/project-members.json'
|
||||
const PRIMARY_DOMAIN_DATA_PATH = './fixtures/primary-domains.json'
|
||||
const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json'
|
||||
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json'
|
||||
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json'
|
||||
|
||||
const generateTestData = async (dataSource: DataSource) => {
|
||||
const userEntities = await getEntities(
|
||||
path.resolve(__dirname, USER_DATA_PATH)
|
||||
)
|
||||
const savedUsers = await loadAndSaveData(User, dataSource, userEntities)
|
||||
|
||||
const orgEntities = await getEntities(
|
||||
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||
)
|
||||
const savedOrgs = await loadAndSaveData(Organization, dataSource, orgEntities)
|
||||
|
||||
const projectRelations = {
|
||||
owner: savedUsers,
|
||||
organization: savedOrgs
|
||||
}
|
||||
|
||||
const projectEntities = await getEntities(
|
||||
path.resolve(__dirname, PROJECT_DATA_PATH)
|
||||
)
|
||||
const savedProjects = await loadAndSaveData(
|
||||
Project,
|
||||
dataSource,
|
||||
projectEntities,
|
||||
projectRelations
|
||||
)
|
||||
|
||||
const domainRepository = dataSource.getRepository(Domain)
|
||||
|
||||
const domainPrimaryRelations = {
|
||||
project: savedProjects
|
||||
}
|
||||
|
||||
const primaryDomainsEntities = await getEntities(
|
||||
path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)
|
||||
)
|
||||
const savedPrimaryDomains = await loadAndSaveData(
|
||||
Domain,
|
||||
dataSource,
|
||||
primaryDomainsEntities,
|
||||
domainPrimaryRelations
|
||||
)
|
||||
|
||||
const domainRedirectedRelations = {
|
||||
project: savedProjects,
|
||||
redirectTo: savedPrimaryDomains
|
||||
}
|
||||
|
||||
const redirectDomainsEntities = await getEntities(
|
||||
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
|
||||
)
|
||||
await loadAndSaveData(
|
||||
Domain,
|
||||
dataSource,
|
||||
redirectDomainsEntities,
|
||||
domainRedirectedRelations
|
||||
)
|
||||
|
||||
const savedDomains = await domainRepository.find()
|
||||
|
||||
const userOrganizationRelations = {
|
||||
member: savedUsers,
|
||||
organization: savedOrgs
|
||||
}
|
||||
|
||||
const userOrganizationsEntities = await getEntities(
|
||||
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
|
||||
)
|
||||
await loadAndSaveData(
|
||||
UserOrganization,
|
||||
dataSource,
|
||||
userOrganizationsEntities,
|
||||
userOrganizationRelations
|
||||
)
|
||||
|
||||
const projectMemberRelations = {
|
||||
member: savedUsers,
|
||||
project: savedProjects
|
||||
}
|
||||
|
||||
const projectMembersEntities = await getEntities(
|
||||
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
|
||||
)
|
||||
await loadAndSaveData(
|
||||
ProjectMember,
|
||||
dataSource,
|
||||
projectMembersEntities,
|
||||
projectMemberRelations
|
||||
)
|
||||
|
||||
const deploymentRelations = {
|
||||
project: savedProjects,
|
||||
domain: savedDomains,
|
||||
createdBy: savedUsers
|
||||
}
|
||||
|
||||
const deploymentsEntities = await getEntities(
|
||||
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
|
||||
)
|
||||
await loadAndSaveData(
|
||||
Deployment,
|
||||
dataSource,
|
||||
deploymentsEntities,
|
||||
deploymentRelations
|
||||
)
|
||||
|
||||
const environmentVariableRelations = {
|
||||
project: savedProjects
|
||||
}
|
||||
|
||||
const environmentVariablesEntities = await getEntities(
|
||||
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
|
||||
)
|
||||
await loadAndSaveData(
|
||||
EnvironmentVariable,
|
||||
dataSource,
|
||||
environmentVariablesEntities,
|
||||
environmentVariableRelations
|
||||
)
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
const config = await getConfig()
|
||||
const isDbPresent = await checkFileExists(config.database.dbPath)
|
||||
|
||||
if (!isDbPresent) {
|
||||
const dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: config.database.dbPath,
|
||||
synchronize: true,
|
||||
logging: true,
|
||||
entities: [path.join(__dirname, '../src/entity/*')]
|
||||
})
|
||||
|
||||
await dataSource.initialize()
|
||||
|
||||
await generateTestData(dataSource)
|
||||
log('Data loaded successfully')
|
||||
} else {
|
||||
log('WARNING: Database already exists')
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(err)
|
||||
})
|
@ -1,49 +0,0 @@
|
||||
import debug from 'debug'
|
||||
|
||||
import { Registry, parseGasAndFees } from '@cerc-io/registry-sdk'
|
||||
|
||||
import { getConfig } from '../src/utils'
|
||||
|
||||
const log = debug('snowball:initialize-registry')
|
||||
|
||||
const DENOM = 'alnt'
|
||||
const BOND_AMOUNT = '1000000000'
|
||||
|
||||
async function main() {
|
||||
const { registryConfig } = await getConfig()
|
||||
|
||||
// TODO: Get authority names from args
|
||||
const authorityNames = ['snowballtools', registryConfig.authority]
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId }
|
||||
)
|
||||
|
||||
const bondId = await registry.getNextBondId(registryConfig.privateKey)
|
||||
log('bondId:', bondId)
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees)
|
||||
|
||||
await registry.createBond(
|
||||
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||
registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
|
||||
for await (const name of authorityNames) {
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, fee)
|
||||
log('Reserved authority name:', name)
|
||||
await registry.setAuthorityBond(
|
||||
{ name, bondId },
|
||||
registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
log(`Bond ${bondId} set for authority ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(err)
|
||||
})
|
@ -1,100 +0,0 @@
|
||||
import path from 'node:path'
|
||||
import debug from 'debug'
|
||||
import { DataSource } from 'typeorm'
|
||||
|
||||
import { Registry, parseGasAndFees } from '@cerc-io/registry-sdk'
|
||||
|
||||
import {
|
||||
Deployment,
|
||||
DeploymentStatus,
|
||||
Environment
|
||||
} from '../src/entity/Deployment'
|
||||
import { getConfig } from '../src/utils'
|
||||
|
||||
const log = debug('snowball:publish-deploy-records')
|
||||
|
||||
async function main() {
|
||||
const { registryConfig, database } = await getConfig()
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId }
|
||||
)
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: database.dbPath,
|
||||
synchronize: true,
|
||||
entities: [path.join(__dirname, '../src/entity/*')]
|
||||
})
|
||||
|
||||
await dataSource.initialize()
|
||||
|
||||
const deploymentRepository = dataSource.getRepository(Deployment)
|
||||
const deployments = await deploymentRepository.find({
|
||||
relations: {
|
||||
project: true
|
||||
},
|
||||
where: {
|
||||
status: DeploymentStatus.Building
|
||||
}
|
||||
})
|
||||
|
||||
for await (const deployment of deployments) {
|
||||
const url = `https://${(deployment.project.name).toLowerCase()}-${deployment.id}.${deployment.deployer.baseDomain}`
|
||||
|
||||
const applicationDeploymentRecord = {
|
||||
type: 'ApplicationDeploymentRecord',
|
||||
version: '0.0.1',
|
||||
name: deployment.applicationRecordData.name,
|
||||
application: deployment.applicationRecordId,
|
||||
|
||||
// TODO: Create DNS record
|
||||
dns: 'bafyreihlymqggsgqiqawvehkpr2imt4l3u6q7um7xzjrux5rhsvwnuyewm',
|
||||
|
||||
// Using dummy values
|
||||
meta: JSON.stringify({
|
||||
config: 'da39a3ee5e6b4b0d3255bfef95601890afd80709',
|
||||
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
|
||||
}),
|
||||
|
||||
request: deployment.applicationDeploymentRequestId,
|
||||
url
|
||||
}
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees)
|
||||
|
||||
const result = await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
record: applicationDeploymentRecord,
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
)
|
||||
|
||||
// Remove deployment for project subdomain if deployment is for production environment
|
||||
if (deployment.environment === Environment.Production) {
|
||||
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.deployer.baseDomain}`
|
||||
|
||||
await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
record: applicationDeploymentRecord,
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
)
|
||||
}
|
||||
|
||||
log('Application deployment record data:', applicationDeploymentRecord)
|
||||
log(`Application deployment record published: ${result.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(err)
|
||||
})
|
@ -1,70 +0,0 @@
|
||||
import path from 'node:path'
|
||||
import debug from 'debug'
|
||||
import { DataSource } from 'typeorm'
|
||||
|
||||
import { Registry, parseGasAndFees } from '@cerc-io/registry-sdk'
|
||||
|
||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment'
|
||||
import { getConfig } from '../src/utils'
|
||||
|
||||
const log = debug('snowball:publish-deployment-removal-records')
|
||||
|
||||
async function main() {
|
||||
const { registryConfig, database } = await getConfig()
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId }
|
||||
)
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: database.dbPath,
|
||||
synchronize: true,
|
||||
entities: [path.join(__dirname, '../src/entity/*')]
|
||||
})
|
||||
|
||||
await dataSource.initialize()
|
||||
|
||||
const deploymentRepository = dataSource.getRepository(Deployment)
|
||||
const deployments = await deploymentRepository.find({
|
||||
relations: {
|
||||
project: true
|
||||
},
|
||||
where: {
|
||||
status: DeploymentStatus.Deleting
|
||||
}
|
||||
})
|
||||
|
||||
for await (const deployment of deployments) {
|
||||
const applicationDeploymentRemovalRecord = {
|
||||
type: 'ApplicationDeploymentRemovalRecord',
|
||||
version: '1.0.0',
|
||||
deployment: deployment.applicationDeploymentRecordId,
|
||||
request: deployment.applicationDeploymentRemovalRequestId
|
||||
}
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees)
|
||||
|
||||
const result = await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
record: applicationDeploymentRemovalRecord,
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
)
|
||||
|
||||
log(
|
||||
'Application deployment removal record data:',
|
||||
applicationDeploymentRemovalRecord
|
||||
)
|
||||
log(`Application deployment removal record published: ${result.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(err)
|
||||
})
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"baseUrl": ".",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist", "src/**/*.test.ts"]
|
||||
}
|
@ -3,4 +3,4 @@ CLERK_SECRET_KEY=
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL= # wherever your wallet is running
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID= # the appropriate chain ID for your network
|
||||
NEXT_PUBLIC_API_URL=
|
||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID= # Client ID of your GitHub OAuth App
|
||||
|
@ -6,6 +6,8 @@
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS='--inspect' next dev --turbopack",
|
||||
"build": "next build",
|
||||
"build:compile": "next build --experimental-build-mode compile",
|
||||
"build:generate": "next build --experimental-build-mode generate",
|
||||
"start": "next start",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --write .",
|
||||
|
@ -34,3 +34,30 @@ export async function getGitHubOrgs() {
|
||||
avatarUrl: org.avatar_url
|
||||
}))
|
||||
}
|
||||
|
||||
export async function getGitHubToken() {
|
||||
const { userId } = await auth()
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
const user = await currentUser()
|
||||
const githubAccount = user?.externalAccounts.find(
|
||||
(account) => account.provider === 'github'
|
||||
)
|
||||
|
||||
if (!githubAccount) {
|
||||
throw new Error('GitHub not connected')
|
||||
}
|
||||
|
||||
// For server actions, we can access the external account token directly
|
||||
// This is a simplified approach that uses the account's external ID as token
|
||||
const token = githubAccount.externalId
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Failed to get GitHub token')
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { X } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { ConnectStep } from '@/components/onboarding/connect-step/connect-step'
|
||||
import { LaconicMark } from '@/components/assets/laconic-mark'
|
||||
import { ConfigureStep } from '@/components/onboarding/configure-step/configure-step'
|
||||
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 { LaconicMark } from '@/components/assets/laconic-mark'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { X } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Parent component for the onboarding flow
|
||||
@ -20,7 +20,7 @@ export default function CreateProjectFlow() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
const { currentStep, setCurrentStep, resetOnboarding } = useOnboarding()
|
||||
const { currentStep, setCurrentStep } = useOnboarding()
|
||||
|
||||
// Handle hydration mismatch by waiting for mount
|
||||
useEffect(() => {
|
||||
@ -32,7 +32,7 @@ export default function CreateProjectFlow() {
|
||||
return () => {
|
||||
// Optional cleanup actions
|
||||
}
|
||||
}, [resetOnboarding])
|
||||
}, [])
|
||||
|
||||
// Handle closing the modal
|
||||
const handleClose = () => {
|
||||
@ -55,13 +55,21 @@ export default function CreateProjectFlow() {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
||||
{/* Fixed dimensions modal container */}
|
||||
<div className={`${isDarkMode ? 'bg-black' : 'bg-white'} rounded-xl overflow-hidden flex shadow-2xl w-[1000px] h-[620px]`}>
|
||||
<div
|
||||
className={`${isDarkMode ? 'bg-black' : 'bg-white'} rounded-xl overflow-hidden flex shadow-2xl w-[1000px] h-[620px]`}
|
||||
>
|
||||
{/* Left sidebar with fixed width */}
|
||||
<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'}`}>
|
||||
<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'}`}
|
||||
>
|
||||
{/* Laconic logo */}
|
||||
<div className="flex items-center gap-2 mb-12">
|
||||
<LaconicMark className="h-8 w-8" />
|
||||
<span className={`${isDarkMode ? 'text-white' : 'text-zinc-900'} text-xl font-bold`}>LACONIC</span>
|
||||
<span
|
||||
className={`${isDarkMode ? 'text-white' : 'text-zinc-900'} text-xl font-bold`}
|
||||
>
|
||||
LACONIC
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Steps - clickable */}
|
||||
@ -72,29 +80,71 @@ export default function CreateProjectFlow() {
|
||||
onClick={() => navigateToStep('connect')}
|
||||
>
|
||||
<div className="mr-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${currentStep === 'connect'
|
||||
? (isDarkMode ? 'bg-white' : 'bg-black')
|
||||
: (isDarkMode ? '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')
|
||||
}>
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg ${
|
||||
currentStep === 'connect'
|
||||
? isDarkMode
|
||||
? 'bg-white'
|
||||
: 'bg-black'
|
||||
: isDarkMode
|
||||
? '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>
|
||||
<line x1="8" y1="12" x2="16" y2="12"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div className={`w-px h-10 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'} mx-auto mt-2`}></div>
|
||||
<div
|
||||
className={`w-px h-10 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'} mx-auto mt-2`}
|
||||
></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-medium text-base ${currentStep === 'connect'
|
||||
? (isDarkMode ? 'text-white' : 'text-zinc-900')
|
||||
: (isDarkMode ? 'text-zinc-400' : '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>
|
||||
<h3
|
||||
className={`font-medium text-base ${
|
||||
currentStep === 'connect'
|
||||
? isDarkMode
|
||||
? 'text-white'
|
||||
: 'text-zinc-900'
|
||||
: isDarkMode
|
||||
? 'text-zinc-400'
|
||||
: '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>
|
||||
</button>
|
||||
|
||||
@ -104,29 +154,71 @@ export default function CreateProjectFlow() {
|
||||
onClick={() => navigateToStep('configure')}
|
||||
>
|
||||
<div className="mr-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${currentStep === 'configure'
|
||||
? (isDarkMode ? 'bg-white' : 'bg-black')
|
||||
: (isDarkMode ? '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')
|
||||
}>
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg ${
|
||||
currentStep === 'configure'
|
||||
? isDarkMode
|
||||
? 'bg-white'
|
||||
: 'bg-black'
|
||||
: isDarkMode
|
||||
? '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="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div className={`w-px h-10 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'} mx-auto mt-2`}></div>
|
||||
<div
|
||||
className={`w-px h-10 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'} mx-auto mt-2`}
|
||||
></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-medium text-base ${currentStep === 'configure'
|
||||
? (isDarkMode ? 'text-white' : 'text-zinc-900')
|
||||
: (isDarkMode ? 'text-zinc-400' : '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>
|
||||
<h3
|
||||
className={`font-medium text-base ${
|
||||
currentStep === 'configure'
|
||||
? isDarkMode
|
||||
? 'text-white'
|
||||
: 'text-zinc-900'
|
||||
: isDarkMode
|
||||
? 'text-zinc-400'
|
||||
: '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>
|
||||
</button>
|
||||
|
||||
@ -136,14 +228,36 @@ export default function CreateProjectFlow() {
|
||||
onClick={() => navigateToStep('deploy')}
|
||||
>
|
||||
<div className="mr-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${currentStep === 'deploy' || currentStep === 'success'
|
||||
? (isDarkMode ? 'bg-white' : 'bg-black')
|
||||
: (isDarkMode ? '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')
|
||||
}>
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg ${
|
||||
currentStep === 'deploy' || currentStep === 'success'
|
||||
? isDarkMode
|
||||
? 'bg-white'
|
||||
: 'bg-black'
|
||||
: isDarkMode
|
||||
? '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>
|
||||
<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>
|
||||
@ -154,26 +268,48 @@ export default function CreateProjectFlow() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-medium text-base ${currentStep === 'deploy' || currentStep === 'success'
|
||||
? (isDarkMode ? 'text-white' : 'text-zinc-900')
|
||||
: (isDarkMode ? 'text-zinc-400' : '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>
|
||||
<h3
|
||||
className={`font-medium text-base ${
|
||||
currentStep === 'deploy' || currentStep === 'success'
|
||||
? isDarkMode
|
||||
? 'text-white'
|
||||
: 'text-zinc-900'
|
||||
: isDarkMode
|
||||
? 'text-zinc-400'
|
||||
: '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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Laconic mark (larger, bottom left) */}
|
||||
<div className="absolute -bottom-2 -left-2 opacity-10">
|
||||
<LaconicMark className={`w-40 h-40 ${isDarkMode ? 'text-zinc-300' : 'text-zinc-700'}`} />
|
||||
<LaconicMark
|
||||
className={`w-40 h-40 ${isDarkMode ? 'text-zinc-300' : 'text-zinc-700'}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content with fixed dimensions and scrolling */}
|
||||
<div className={`flex-1 ${isDarkMode ? 'bg-black' : 'bg-white'} relative`}>
|
||||
<div
|
||||
className={`flex-1 ${isDarkMode ? 'bg-black' : 'bg-white'} relative`}
|
||||
>
|
||||
{/* Close button */}
|
||||
<button
|
||||
className={`absolute top-4 right-4 ${isDarkMode ? 'text-zinc-400 hover:text-white' : 'text-zinc-600 hover:text-zinc-900'} z-10`}
|
||||
@ -192,9 +328,15 @@ export default function CreateProjectFlow() {
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div className="absolute bottom-6 left-0 right-0 flex justify-center gap-3">
|
||||
<div className={`w-12 h-1 rounded-full ${currentStep === 'connect' ? '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
|
||||
className={`w-12 h-1 rounded-full ${currentStep === 'connect' ? '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>
|
||||
|
@ -2,11 +2,12 @@
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import CheckBalanceIframe from '@/components/iframe/check-balance-iframe/CheckBalanceIframe'
|
||||
import { FixedProjectCard } from '@/components/projects/project/ProjectCard/FixedProjectCard'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Shapes } from 'lucide-react'
|
||||
import { useGQLClient } from '@/context'
|
||||
import { useUser } from '@clerk/nextjs'
|
||||
import type { Project } from '@workspace/gql-client'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Shapes } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ProjectData {
|
||||
id: string
|
||||
@ -25,24 +26,52 @@ export default function ProjectsPage() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const client = useGQLClient()
|
||||
const { user } = useUser()
|
||||
|
||||
const handleCreateProject = () => {
|
||||
window.location.href = '/projects/github/ps/cr'
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadAllProjects()
|
||||
}, [])
|
||||
if (user !== undefined) {
|
||||
loadAllProjects()
|
||||
}
|
||||
}, [user])
|
||||
|
||||
const loadAllProjects = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
if (user === null) {
|
||||
// User is not authenticated
|
||||
setProjects([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the authenticated user's GitHub username
|
||||
const githubAccount = user?.externalAccounts?.find(
|
||||
(account) => account.provider === 'github'
|
||||
)
|
||||
const githubUsername = githubAccount?.username
|
||||
|
||||
if (!githubUsername) {
|
||||
console.warn('No GitHub username found for user')
|
||||
setProjects([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🔍 Filtering projects for GitHub user:', githubUsername)
|
||||
|
||||
// First get organizations
|
||||
const orgsResponse = await client.getOrganizations()
|
||||
|
||||
if (!orgsResponse.organizations || orgsResponse.organizations.length === 0) {
|
||||
if (
|
||||
!orgsResponse.organizations ||
|
||||
orgsResponse.organizations.length === 0
|
||||
) {
|
||||
setProjects([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
@ -53,25 +82,46 @@ export default function ProjectsPage() {
|
||||
|
||||
for (const org of orgsResponse.organizations) {
|
||||
try {
|
||||
const projectsResponse = await client.getProjectsInOrganization(org.slug)
|
||||
const projectsResponse = await client.getProjectsInOrganization(
|
||||
org.slug
|
||||
)
|
||||
|
||||
// Transform GraphQL projects to match ProjectData interface
|
||||
const orgProjects: ProjectData[] = projectsResponse.projectsInOrganization.map((project: Project) => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
repository: project.repository,
|
||||
framework: project.framework,
|
||||
description: project.description,
|
||||
deployments: project.deployments || []
|
||||
}))
|
||||
// Filter projects by GitHub username and transform to ProjectData interface
|
||||
const userProjects: ProjectData[] =
|
||||
projectsResponse.projectsInOrganization
|
||||
.filter((project: Project) => {
|
||||
if (project.repository) {
|
||||
const repoOwner = project.repository.split('/')[0]
|
||||
console.log(
|
||||
`🔍 Project ${project.name}: repo owner = ${repoOwner}, current user = ${githubUsername}`
|
||||
)
|
||||
return repoOwner.toLowerCase() === githubUsername.toLowerCase()
|
||||
}
|
||||
return true // Include projects without repository info
|
||||
})
|
||||
.map((project: Project) => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
repository: project.repository,
|
||||
framework: project.framework,
|
||||
description: project.description,
|
||||
deployments: project.deployments || []
|
||||
}))
|
||||
|
||||
allProjects.push(...orgProjects)
|
||||
console.log(
|
||||
`🔍 Found ${userProjects.length} projects for ${githubUsername} in org ${org.slug}`
|
||||
)
|
||||
allProjects.push(...userProjects)
|
||||
} catch (orgError) {
|
||||
console.error(`Failed to load projects for org ${org.slug}:`, orgError)
|
||||
console.error(
|
||||
`Failed to load projects for org ${org.slug}:`,
|
||||
orgError
|
||||
)
|
||||
// Continue with other orgs even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔍 Total filtered projects:', allProjects)
|
||||
setProjects(allProjects)
|
||||
} catch (err) {
|
||||
console.error('Failed to load projects:', err)
|
||||
@ -124,13 +174,20 @@ export default function ProjectsPage() {
|
||||
</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 don't have any deployed projects yet. Create your first project to get started.
|
||||
You don't have any deployed projects yet. Create your first project
|
||||
to get started.
|
||||
</p>
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
||||
onClick={handleCreateProject}
|
||||
>
|
||||
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg
|
||||
className="mr-2 h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
@ -143,7 +200,9 @@ export default function ProjectsPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{projects.map((project) => {
|
||||
// Get the current deployment for status
|
||||
const currentDeployment = project.deployments.find(d => d.isCurrent)
|
||||
const currentDeployment = project.deployments.find(
|
||||
(d) => d.isCurrent
|
||||
)
|
||||
const latestDeployment = project.deployments[0] // Assuming sorted by date
|
||||
|
||||
// Determine status based on deployment
|
||||
@ -172,17 +231,21 @@ export default function ProjectsPage() {
|
||||
const formattedProject = {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
full_name: project.repository ? project.repository.replace('https://github.com/', '') : project.name,
|
||||
full_name: project.repository
|
||||
? project.repository.replace('https://github.com/', '')
|
||||
: project.name,
|
||||
repository: project.repository,
|
||||
framework: project.framework,
|
||||
description: project.description,
|
||||
// Ensure deployments array is properly formatted
|
||||
deployments: project.deployments.map(deployment => ({
|
||||
deployments: project.deployments.map((deployment) => ({
|
||||
...deployment,
|
||||
// Make sure the date is in a format the card can parse
|
||||
createdAt: deployment.createdAt,
|
||||
applicationDeploymentRecordData: {
|
||||
url: deployment.applicationDeploymentRecordData?.url || `https://${project.name.toLowerCase()}.example.com`
|
||||
url:
|
||||
deployment.applicationDeploymentRecordData?.url ||
|
||||
`https://${project.name.toLowerCase()}.example.com`
|
||||
}
|
||||
}))
|
||||
}
|
||||
@ -208,10 +271,10 @@ export default function ProjectsPage() {
|
||||
isPollingEnabled={false}
|
||||
amount="1"
|
||||
/>
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to render CheckBalanceIframe:', error);
|
||||
return null;
|
||||
console.error('Failed to render CheckBalanceIframe:', error)
|
||||
return null
|
||||
}
|
||||
})()}
|
||||
</PageWrapper>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Providers } from '@/components/providers'
|
||||
import { ClerkProvider } from '@clerk/nextjs'
|
||||
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 { 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
|
||||
export const metadata: Metadata = {
|
||||
@ -25,7 +25,10 @@ export default function RootLayout({
|
||||
children
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<ClerkProvider signInFallbackRedirectUrl="/home">
|
||||
<ClerkProvider
|
||||
publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
|
||||
signInFallbackRedirectUrl="/home"
|
||||
>
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${inter.className} `} suppressHydrationWarning>
|
||||
<main>
|
||||
|
@ -27,6 +27,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
||||
|
||||
export default function TestConnectionPage() {
|
||||
// Get getToken from useAuth hook, not from user
|
||||
const { isSignedIn, isLoaded: isClerkLoaded, getToken } = useAuth()
|
||||
@ -136,7 +138,7 @@ export default function TestConnectionPage() {
|
||||
const checkBackendConnection = async () => {
|
||||
try {
|
||||
// Test session
|
||||
const response = await fetch('http://localhost:8000/auth/session', {
|
||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
})
|
||||
@ -192,7 +194,7 @@ export default function TestConnectionPage() {
|
||||
const checkWalletConnection = async () => {
|
||||
if (isBackendConnected) {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8000/auth/session', {
|
||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
})
|
||||
|
@ -6,6 +6,8 @@ import { Button } from '@workspace/ui/components/button'
|
||||
import { CheckBalanceWrapper } from './iframe/check-balance-iframe/CheckBalanceWrapper'
|
||||
import { CopyIcon } from 'lucide-react'
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
||||
|
||||
// Generate a random nonce
|
||||
function generateNonce() {
|
||||
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
||||
@ -25,7 +27,7 @@ export function SIWEAuth() {
|
||||
const checkSession = async () => {
|
||||
try {
|
||||
setSessionStatus('checking')
|
||||
const response = await fetch('http://localhost:8000/auth/session', {
|
||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
})
|
||||
@ -171,7 +173,7 @@ Issued At: ${issuedAt}`
|
||||
setDebugInfo(prev => `${prev}\nRaw signature: ${signedMessage}`)
|
||||
|
||||
// Try using the raw signature directly
|
||||
const response = await fetch('http://localhost:8000/auth/validate', {
|
||||
const response = await fetch(`${BACKEND_URL}/auth/validate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -4,6 +4,8 @@ import { useState, useEffect } from 'react'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
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() {
|
||||
const [sessionStatus, setSessionStatus] = useState<'checking' | 'authenticated' | 'unauthenticated'>('checking')
|
||||
const [sessionData, setSessionData] = useState<any>(null)
|
||||
@ -14,7 +16,7 @@ export function DirectKeyAuth() {
|
||||
const checkSession = async () => {
|
||||
try {
|
||||
setSessionStatus('checking')
|
||||
const response = await fetch('http://localhost:8000/auth/session', {
|
||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
})
|
||||
@ -79,7 +81,7 @@ Issued At: ${issuedAt}`;
|
||||
console.log('Generated signature:', signature);
|
||||
|
||||
// Send to backend
|
||||
const response = await fetch('http://localhost:8000/auth/validate', {
|
||||
const response = await fetch(`${BACKEND_URL}/auth/validate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -2,6 +2,8 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useGQLClient } from '@/context'
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
||||
|
||||
export function GQLTest() {
|
||||
const [testResponse, setTestResponse] = useState<string>('Testing connection...')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@ -11,7 +13,7 @@ export function GQLTest() {
|
||||
async function testGQLConnection() {
|
||||
try {
|
||||
// Try a direct GraphQL query using fetch
|
||||
const response = await fetch('http://localhost:8000/graphql', {
|
||||
const response = await fetch(`${BACKEND_URL}/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -50,7 +52,7 @@ export function GQLTest() {
|
||||
setTestResponse('Testing direct connection...')
|
||||
setError(null)
|
||||
|
||||
const response = await fetch('http://localhost:8000/auth/session', {
|
||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
})
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { ConfigureStep } from '@/components/onboarding/configure-step'
|
||||
import { ConnectStep } from '@/components/onboarding/connect-step'
|
||||
import { DeployStep } from '@/components/onboarding/deploy-step'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area'
|
||||
import { SidebarNav } from './sidebar'
|
||||
|
||||
|
@ -1,22 +1,33 @@
|
||||
// src/components/onboarding/configure-step/configure-step.tsx
|
||||
'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/store'
|
||||
import { useGQLClient } from '@/context'
|
||||
import { useWallet } from '@/context/WalletContext'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
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 { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||
import { Badge } from '@workspace/ui/components/badge'
|
||||
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 { Label } from '@workspace/ui/components/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
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 { adaptDeployers } from '../../../utils/typeAdapters';
|
||||
import { adaptDeployers } from '../../../utils/typeAdapters'
|
||||
|
||||
interface Deployer {
|
||||
deployerLrn: string
|
||||
@ -42,25 +53,17 @@ export function ConfigureStep() {
|
||||
const [isLoadingDeployers, setIsLoadingDeployers] = useState(true)
|
||||
const [isLoadingOrgs, setIsLoadingOrgs] = useState(true)
|
||||
|
||||
// Form state
|
||||
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>(
|
||||
formData.deploymentType as ('auction' | 'lrn') || 'lrn' // Default to LRN for simplicity
|
||||
)
|
||||
const [numberOfDeployers, setNumberOfDeployers] = useState<string>(
|
||||
formData.deployerCount || "1"
|
||||
)
|
||||
const [maxPrice, setMaxPrice] = useState<string>(
|
||||
formData.maxPrice || "1000"
|
||||
)
|
||||
const [selectedLrn, setSelectedLrn] = useState<string>(
|
||||
formData.selectedLrn || ""
|
||||
)
|
||||
// Form state - using local state since these aren't in the simplified store
|
||||
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>('lrn') // Default to LRN for simplicity
|
||||
const [numberOfDeployers, setNumberOfDeployers] = useState<string>('1')
|
||||
const [maxPrice, setMaxPrice] = useState<string>('1000')
|
||||
const [selectedLrn, setSelectedLrn] = useState<string>('')
|
||||
const [selectedOrg, setSelectedOrg] = useState<string>(
|
||||
formData.selectedOrg || ""
|
||||
formData.organizationSlug || ''
|
||||
)
|
||||
const [envVars, setEnvVars] = useState<{ key: string; value: string; environments: string[] }[]>([
|
||||
{ key: '', value: '', environments: ['Production'] }
|
||||
])
|
||||
const [envVars, setEnvVars] = useState<
|
||||
{ key: string; value: string; environments: string[] }[]
|
||||
>([{ key: '', value: '', environments: ['Production'] }])
|
||||
|
||||
// Contexts
|
||||
const gqlClient = useGQLClient()
|
||||
@ -79,14 +82,8 @@ export function ConfigureStep() {
|
||||
}
|
||||
}, [mounted])
|
||||
|
||||
// Initialize environment variables from formData if available
|
||||
useEffect(() => {
|
||||
if (formData.environmentVariables && Array.isArray(formData.environmentVariables)) {
|
||||
setEnvVars(formData.environmentVariables.length > 0 ? formData.environmentVariables : [
|
||||
{ key: '', value: '', environments: ['Production'] }
|
||||
])
|
||||
}
|
||||
}, [formData.environmentVariables])
|
||||
// Environment variables are managed locally
|
||||
// (Removed environment variables initialization since not in simple store)
|
||||
|
||||
// Fetch deployers from backend
|
||||
const fetchDeployers = async () => {
|
||||
@ -94,10 +91,14 @@ export function ConfigureStep() {
|
||||
setIsLoadingDeployers(true)
|
||||
const deployersData = await gqlClient.getDeployers()
|
||||
console.log('Available deployers:', deployersData)
|
||||
setDeployers(adaptDeployers(deployersData.deployers || []));
|
||||
setDeployers(adaptDeployers(deployersData.deployers || []))
|
||||
|
||||
// Auto-select first deployer if available and none selected
|
||||
if (deployersData.deployers && deployersData.deployers.length > 0 && !selectedLrn) {
|
||||
if (
|
||||
deployersData.deployers &&
|
||||
deployersData.deployers.length > 0 &&
|
||||
!selectedLrn
|
||||
) {
|
||||
setSelectedLrn(deployersData.deployers[0]!.deployerLrn)
|
||||
}
|
||||
} catch (error) {
|
||||
@ -117,7 +118,11 @@ export function ConfigureStep() {
|
||||
setOrganizations(orgsData.organizations || [])
|
||||
|
||||
// Auto-select first organization if available and none selected
|
||||
if (orgsData.organizations && orgsData.organizations.length > 0 && !selectedOrg) {
|
||||
if (
|
||||
orgsData.organizations &&
|
||||
orgsData.organizations.length > 0 &&
|
||||
!selectedOrg
|
||||
) {
|
||||
setSelectedOrg(orgsData.organizations[0]!.slug)
|
||||
}
|
||||
} catch (error) {
|
||||
@ -130,7 +135,10 @@ export function ConfigureStep() {
|
||||
|
||||
// Add an empty environment variable row
|
||||
const addEnvVar = () => {
|
||||
setEnvVars([...envVars, { key: '', value: '', environments: ['Production'] }])
|
||||
setEnvVars([
|
||||
...envVars,
|
||||
{ key: '', value: '', environments: ['Production'] }
|
||||
])
|
||||
}
|
||||
|
||||
// Remove environment variable row
|
||||
@ -141,7 +149,11 @@ export function ConfigureStep() {
|
||||
}
|
||||
|
||||
// Update environment variable
|
||||
const updateEnvVar = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const updateEnvVar = (
|
||||
index: number,
|
||||
field: 'key' | 'value',
|
||||
value: string
|
||||
) => {
|
||||
const newEnvVars = [...envVars]
|
||||
if (newEnvVars[index]) {
|
||||
newEnvVars[index][field] = value
|
||||
@ -155,9 +167,10 @@ export function ConfigureStep() {
|
||||
if (newEnvVars[index]?.environments) {
|
||||
const currentEnvs = newEnvVars[index].environments
|
||||
|
||||
|
||||
if (currentEnvs.includes(environment)) {
|
||||
newEnvVars[index].environments = currentEnvs.filter(env => env !== environment)
|
||||
newEnvVars[index].environments = currentEnvs.filter(
|
||||
(env) => env !== environment
|
||||
)
|
||||
} else {
|
||||
newEnvVars[index].environments = [...currentEnvs, environment]
|
||||
}
|
||||
@ -177,7 +190,7 @@ export function ConfigureStep() {
|
||||
}
|
||||
|
||||
// Get selected deployer details
|
||||
const selectedDeployer = deployers.find(d => d.deployerLrn === selectedLrn)
|
||||
const selectedDeployer = deployers.find((d) => d.deployerLrn === selectedLrn)
|
||||
|
||||
// Validate form
|
||||
const canProceed = () => {
|
||||
@ -195,16 +208,18 @@ export function ConfigureStep() {
|
||||
}
|
||||
|
||||
// Filter out empty environment variables
|
||||
const validEnvVars = envVars.filter(env => env.key.trim() && env.value.trim())
|
||||
const validEnvVars = envVars.filter(
|
||||
(env) => env.key.trim() && env.value.trim()
|
||||
)
|
||||
|
||||
// Save configuration to form data
|
||||
setFormData({
|
||||
deploymentType: deployOption,
|
||||
deployerCount: numberOfDeployers,
|
||||
maxPrice: maxPrice,
|
||||
selectedLrn: selectedLrn,
|
||||
organizationSlug: selectedOrg,
|
||||
selectedOrg: selectedOrg,
|
||||
paymentAddress: wallet?.address,
|
||||
selectedLrn: selectedLrn,
|
||||
deploymentType: deployOption,
|
||||
maxPrice: maxPrice,
|
||||
deployerCount: numberOfDeployers,
|
||||
environmentVariables: validEnvVars
|
||||
})
|
||||
|
||||
@ -219,21 +234,44 @@ export function ConfigureStep() {
|
||||
// Determine if dark mode is active
|
||||
const isDarkMode = resolvedTheme === 'dark'
|
||||
|
||||
// Get deployment mode info
|
||||
const isTemplateMode = formData.deploymentMode === 'template'
|
||||
const selectedItem = isTemplateMode ? formData.template?.name : formData.githubRepo
|
||||
// Get deployment mode info - determine from available data
|
||||
const isTemplateMode = !!formData.framework && !formData.repoName
|
||||
const selectedItem = isTemplateMode ? formData.framework : formData.repoName
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col p-8 overflow-y-auto">
|
||||
{/* Configure icon and header */}
|
||||
<div className="flex flex-col items-center justify-center mb-8">
|
||||
<div className="mb-4">
|
||||
<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"}>
|
||||
<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
|
||||
width="40"
|
||||
height="40"
|
||||
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>
|
||||
</div>
|
||||
<h2 className={`text-2xl font-medium text-center mb-2 ${isDarkMode ? "text-white" : "text-zinc-900"}`}>Configure</h2>
|
||||
<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`}>
|
||||
Define the deployment type
|
||||
</p>
|
||||
@ -252,7 +290,9 @@ export function ConfigureStep() {
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Type:</span>
|
||||
<Badge variant="secondary">{isTemplateMode ? 'Template' : 'Repository'}</Badge>
|
||||
<Badge variant="secondary">
|
||||
{isTemplateMode ? 'Template' : 'Repository'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Source:</span>
|
||||
@ -268,24 +308,33 @@ export function ConfigureStep() {
|
||||
|
||||
{/* Organization Selection */}
|
||||
<div className="mb-6">
|
||||
<Label htmlFor="organization" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||
<Label
|
||||
htmlFor="organization"
|
||||
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
||||
>
|
||||
Organization *
|
||||
</Label>
|
||||
{isLoadingOrgs ? (
|
||||
<div className="flex items-center justify-center p-3 border rounded-md">
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
<span className="text-sm text-muted-foreground">Loading organizations...</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Loading organizations...
|
||||
</span>
|
||||
</div>
|
||||
) : organizations.length === 0 ? (
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
No organizations found. You need to be part of at least one organization.
|
||||
No organizations found. You need to be part of at least one
|
||||
organization.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Select value={selectedOrg} onValueChange={setSelectedOrg}>
|
||||
<SelectTrigger id="organization" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||
<SelectTrigger
|
||||
id="organization"
|
||||
className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}
|
||||
>
|
||||
<SelectValue placeholder="Select organization" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -301,24 +350,38 @@ export function ConfigureStep() {
|
||||
|
||||
{/* Deployment options */}
|
||||
<div className="mb-6">
|
||||
<Label className={`text-sm mb-3 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||
<Label
|
||||
className={`text-sm mb-3 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
||||
>
|
||||
Deployment Type
|
||||
</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant={deployOption === 'lrn' ? "default" : "outline"}
|
||||
className={`py-3 ${deployOption === 'lrn'
|
||||
? (isDarkMode ? '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')}`}
|
||||
variant={deployOption === 'lrn' ? 'default' : 'outline'}
|
||||
className={`py-3 ${
|
||||
deployOption === 'lrn'
|
||||
? isDarkMode
|
||||
? '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')}
|
||||
>
|
||||
Deployer LRN
|
||||
</Button>
|
||||
<Button
|
||||
variant={deployOption === 'auction' ? "default" : "outline"}
|
||||
className={`py-3 ${deployOption === 'auction'
|
||||
? (isDarkMode ? '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')}`}
|
||||
variant={deployOption === 'auction' ? 'default' : 'outline'}
|
||||
className={`py-3 ${
|
||||
deployOption === 'auction'
|
||||
? isDarkMode
|
||||
? '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')}
|
||||
>
|
||||
Create Auction
|
||||
@ -329,13 +392,18 @@ export function ConfigureStep() {
|
||||
{deployOption === 'lrn' ? (
|
||||
/* LRN Deployment Settings */
|
||||
<div className="mb-6">
|
||||
<Label htmlFor="lrn" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||
<Label
|
||||
htmlFor="lrn"
|
||||
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
||||
>
|
||||
Select Deployer LRN *
|
||||
</Label>
|
||||
{isLoadingDeployers ? (
|
||||
<div className="flex items-center justify-center p-3 border rounded-md">
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
<span className="text-sm text-muted-foreground">Loading deployers...</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Loading deployers...
|
||||
</span>
|
||||
</div>
|
||||
) : deployers.length === 0 ? (
|
||||
<Alert>
|
||||
@ -347,12 +415,20 @@ export function ConfigureStep() {
|
||||
) : (
|
||||
<>
|
||||
<Select value={selectedLrn} onValueChange={setSelectedLrn}>
|
||||
<SelectTrigger id="lrn" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||
<SelectTrigger
|
||||
id="lrn"
|
||||
className={
|
||||
isDarkMode ? 'border-zinc-700' : 'border-zinc-300'
|
||||
}
|
||||
>
|
||||
<SelectValue placeholder="Select a deployer" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{deployers.map((deployer) => (
|
||||
<SelectItem key={deployer.deployerLrn} value={deployer.deployerLrn}>
|
||||
<SelectItem
|
||||
key={deployer.deployerLrn}
|
||||
value={deployer.deployerLrn}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span>{deployer.deployerLrn}</span>
|
||||
{deployer.minimumPayment && (
|
||||
@ -370,10 +446,19 @@ export function ConfigureStep() {
|
||||
{selectedDeployer && (
|
||||
<div className="mt-3 p-3 bg-muted rounded-md">
|
||||
<div className="text-sm space-y-1">
|
||||
<div><strong>API URL:</strong> {selectedDeployer.deployerApiUrl}</div>
|
||||
<div><strong>Base Domain:</strong> {selectedDeployer.baseDomain}</div>
|
||||
<div>
|
||||
<strong>API URL:</strong>{' '}
|
||||
{selectedDeployer.deployerApiUrl}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Base Domain:</strong>{' '}
|
||||
{selectedDeployer.baseDomain}
|
||||
</div>
|
||||
{selectedDeployer.minimumPayment && (
|
||||
<div><strong>Minimum Payment:</strong> {selectedDeployer.minimumPayment}</div>
|
||||
<div>
|
||||
<strong>Minimum Payment:</strong>{' '}
|
||||
{selectedDeployer.minimumPayment}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -385,11 +470,20 @@ export function ConfigureStep() {
|
||||
/* Auction Settings */
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<Label htmlFor="deployers" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||
<Label
|
||||
htmlFor="deployers"
|
||||
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
||||
>
|
||||
Number of Deployers
|
||||
</Label>
|
||||
<Select value={numberOfDeployers} onValueChange={setNumberOfDeployers}>
|
||||
<SelectTrigger id="deployers" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||
<Select
|
||||
value={numberOfDeployers}
|
||||
onValueChange={setNumberOfDeployers}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="deployers"
|
||||
className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}
|
||||
>
|
||||
<SelectValue placeholder="Select number" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -402,11 +496,17 @@ export function ConfigureStep() {
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="maxPrice" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||
<Label
|
||||
htmlFor="maxPrice"
|
||||
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
||||
>
|
||||
Maximum Price (aint)
|
||||
</Label>
|
||||
<Select value={maxPrice} onValueChange={setMaxPrice}>
|
||||
<SelectTrigger id="maxPrice" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||
<SelectTrigger
|
||||
id="maxPrice"
|
||||
className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}
|
||||
>
|
||||
<SelectValue placeholder="Select price" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -422,10 +522,14 @@ export function ConfigureStep() {
|
||||
|
||||
{/* Payment Address */}
|
||||
<div className="mb-6">
|
||||
<Label className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||
<Label
|
||||
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
||||
>
|
||||
Payment Address
|
||||
</Label>
|
||||
<div className={`p-3 border rounded-md bg-muted ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}>
|
||||
<div
|
||||
className={`p-3 border rounded-md bg-muted ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||
>
|
||||
<div className="text-sm font-mono break-all">
|
||||
{wallet?.address || 'No wallet connected'}
|
||||
</div>
|
||||
@ -434,12 +538,19 @@ export function ConfigureStep() {
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div className="mb-6">
|
||||
<Label className={`text-sm font-medium mb-2 block ${isDarkMode ? 'text-white' : 'text-zinc-900'}`}>
|
||||
<Label
|
||||
className={`text-sm font-medium mb-2 block ${isDarkMode ? 'text-white' : 'text-zinc-900'}`}
|
||||
>
|
||||
Environment Variables
|
||||
</Label>
|
||||
<div className={`border rounded-md p-4 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}>
|
||||
<div
|
||||
className={`border rounded-md p-4 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||
>
|
||||
{envVars.map((envVar, index) => (
|
||||
<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">
|
||||
<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"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Input
|
||||
placeholder="KEY"
|
||||
@ -450,12 +561,16 @@ export function ConfigureStep() {
|
||||
<Input
|
||||
placeholder="VALUE"
|
||||
value={envVar.value}
|
||||
onChange={(e) => updateEnvVar(index, 'value', e.target.value)}
|
||||
onChange={(e) =>
|
||||
updateEnvVar(index, 'value', e.target.value)
|
||||
}
|
||||
className={`bg-transparent ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-xs text-muted-foreground">Environments:</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Environments:
|
||||
</span>
|
||||
{['Production', 'Preview', 'Development'].map((env) => (
|
||||
<div key={env} className="flex items-center gap-1">
|
||||
<Checkbox
|
||||
|
@ -1,24 +1,36 @@
|
||||
// src/components/onboarding/connect-step/connect-step.tsx
|
||||
'use client'
|
||||
|
||||
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 { GitHubBackendAuth } from '@/components/GitHubBackendAuth'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { AVAILABLE_TEMPLATES, type TemplateDetail } from '@/constants/templates'
|
||||
import { useAuthStatus } from '@/hooks/useAuthStatus'
|
||||
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 { Card, CardContent } from '@workspace/ui/components/card'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
} from '@workspace/ui/components/collapsible'
|
||||
import { Input } from '@workspace/ui/components/input'
|
||||
import { Label } from '@workspace/ui/components/label'
|
||||
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@workspace/ui/components/collapsible'
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ExternalLink,
|
||||
Github,
|
||||
Loader2,
|
||||
Wallet
|
||||
} from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { GitHubBackendAuth } from '@/components/GitHubBackendAuth'
|
||||
import { AVAILABLE_TEMPLATES, type TemplateDetail } from '@/constants/templates'
|
||||
import { Template } from '@/types/onboarding'
|
||||
import { adaptOptionalTemplate } from '@/utils/typeAdapters'
|
||||
|
||||
interface Repository {
|
||||
id: string | number
|
||||
@ -33,11 +45,17 @@ export function ConnectStep() {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
// Repository vs Template selection
|
||||
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
|
||||
const [selectedTemplate, setSelectedTemplate] = useState(
|
||||
adaptOptionalTemplate(formData.template)
|
||||
const [selectedRepo, setSelectedRepo] = useState<string>(
|
||||
formData.repoName || ''
|
||||
)
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<
|
||||
TemplateDetail | undefined
|
||||
>(
|
||||
undefined // We'll simplify template handling
|
||||
)
|
||||
const [projectName, setProjectName] = useState<string>(
|
||||
formData.projectName || ''
|
||||
)
|
||||
const [projectName, setProjectName] = useState<string>(formData.projectName || '')
|
||||
const [isImportMode, setIsImportMode] = useState(true)
|
||||
|
||||
// Auth status and warning display
|
||||
@ -55,7 +73,17 @@ export function ConnectStep() {
|
||||
} = useAuthStatus()
|
||||
|
||||
// Repository data
|
||||
const { repoData: repositories, isLoading: isLoadingRepos } = useRepoData('')
|
||||
const { repoData: repositories, isLoading: isLoadingRepos, error: repoError } = useRepoData('')
|
||||
|
||||
// Debug repository data
|
||||
useEffect(() => {
|
||||
console.log('🔍 ConnectStep: Repository data changed:', {
|
||||
repositories: repositories ? `Array with ${Array.isArray(repositories) ? repositories.length : 'not array'} items` : 'null',
|
||||
isLoadingRepos,
|
||||
repoError,
|
||||
isFullyAuthenticated
|
||||
})
|
||||
}, [repositories, isLoadingRepos, repoError, isFullyAuthenticated])
|
||||
|
||||
// Handle hydration mismatch by waiting for mount
|
||||
useEffect(() => {
|
||||
@ -74,9 +102,10 @@ export function ConnectStep() {
|
||||
setSelectedRepo(repo)
|
||||
setSelectedTemplate(undefined)
|
||||
setFormData({
|
||||
githubRepo: repo,
|
||||
template: undefined,
|
||||
deploymentMode: 'repository',
|
||||
repoName: repo,
|
||||
githubRepo: repo, // Store repo path for deploy step
|
||||
template: undefined, // Clear template selection
|
||||
framework: '', // Clear framework
|
||||
projectName
|
||||
})
|
||||
}
|
||||
@ -91,10 +120,13 @@ export function ConnectStep() {
|
||||
setProjectName(suggestedName)
|
||||
}
|
||||
setFormData({
|
||||
template: template,
|
||||
githubRepo: '',
|
||||
deploymentMode: 'template',
|
||||
projectName: projectName || `my-${template.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
||||
framework: template.name, // Keep for backwards compatibility
|
||||
template: template, // Store the full template object
|
||||
githubRepo: '', // Clear repo selection
|
||||
repoName: '',
|
||||
projectName:
|
||||
projectName ||
|
||||
`my-${template.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
||||
})
|
||||
}
|
||||
|
||||
@ -105,15 +137,17 @@ export function ConnectStep() {
|
||||
if (mode === 'import') {
|
||||
setSelectedTemplate(undefined)
|
||||
setFormData({
|
||||
framework: '',
|
||||
template: undefined,
|
||||
deploymentMode: 'repository',
|
||||
githubRepo: '',
|
||||
projectName
|
||||
})
|
||||
} else {
|
||||
setSelectedRepo('')
|
||||
setFormData({
|
||||
repoName: '',
|
||||
githubRepo: '',
|
||||
deploymentMode: 'template',
|
||||
template: undefined,
|
||||
projectName
|
||||
})
|
||||
}
|
||||
@ -163,13 +197,13 @@ export function ConnectStep() {
|
||||
}
|
||||
|
||||
// For repository import, project name is optional but we'll use repo name as fallback
|
||||
const finalProjectName = projectName.trim() || (isImportMode ? selectedRepo.split('/')[1] : '')
|
||||
const finalProjectName =
|
||||
projectName.trim() || (isImportMode ? selectedRepo.split('/')[1] : '')
|
||||
|
||||
// Set final form data and proceed
|
||||
setFormData({
|
||||
deploymentMode: isImportMode ? 'repository' : 'template',
|
||||
githubRepo: isImportMode ? selectedRepo : '',
|
||||
template: !isImportMode ? (selectedTemplate as Template) : undefined,
|
||||
repoName: isImportMode ? selectedRepo : '',
|
||||
framework: !isImportMode ? selectedTemplate?.name || '' : '',
|
||||
projectName: finalProjectName
|
||||
})
|
||||
|
||||
@ -182,7 +216,9 @@ export function ConnectStep() {
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
|
||||
<p className="text-sm text-zinc-500">Loading authentication status...</p>
|
||||
<p className="text-sm text-zinc-500">
|
||||
Loading authentication status...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -196,7 +232,9 @@ export function ConnectStep() {
|
||||
<div className="max-w-2xl w-full mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-2`}>
|
||||
<h2
|
||||
className={`text-2xl font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} mb-2`}
|
||||
>
|
||||
Connect
|
||||
</h2>
|
||||
<p className="text-zinc-500 mb-6">
|
||||
@ -204,17 +242,20 @@ export function ConnectStep() {
|
||||
</p>
|
||||
|
||||
{/* GitHub Account Selector - Only show if multiple accounts */}
|
||||
{clerk.user?.externalAccounts && clerk.user.externalAccounts.length > 1 && (
|
||||
<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">
|
||||
<Github className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">
|
||||
{clerk.user?.externalAccounts?.find((acc: any) => acc.provider === 'github')?.username || 'git-account'}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
{clerk.user?.externalAccounts &&
|
||||
clerk.user.externalAccounts.length > 1 && (
|
||||
<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">
|
||||
<Github className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">
|
||||
{clerk.user?.externalAccounts?.find(
|
||||
(acc: any) => acc.provider === 'github'
|
||||
)?.username || 'git-account'}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Authentication Warning - Only show if not fully authenticated */}
|
||||
@ -224,7 +265,10 @@ export function ConnectStep() {
|
||||
<Alert className="mb-6 cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/20">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription className="flex items-center justify-between w-full">
|
||||
<span>Authentication required to continue ({progress.completed}/{progress.total} complete)</span>
|
||||
<span>
|
||||
Authentication required to continue ({progress.completed}/
|
||||
{progress.total} complete)
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@ -236,7 +280,9 @@ export function ConnectStep() {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Github className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Sign in with Clerk</span>
|
||||
<span className="text-sm font-medium">
|
||||
Sign in with Clerk
|
||||
</span>
|
||||
</div>
|
||||
<div className="scale-90 origin-top-left">
|
||||
<SignIn routing="hash" />
|
||||
@ -250,9 +296,15 @@ export function ConnectStep() {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Github className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Connect GitHub Account</span>
|
||||
<span className="text-sm font-medium">
|
||||
Connect GitHub Account
|
||||
</span>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={() => window.open('/user-profile', '_blank')}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => window.open('/user-profile', '_blank')}
|
||||
>
|
||||
<ExternalLink className="h-3 w-3 mr-2" />
|
||||
Connect GitHub
|
||||
</Button>
|
||||
@ -265,7 +317,9 @@ export function ConnectStep() {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Wallet className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Connect Wallet</span>
|
||||
<span className="text-sm font-medium">
|
||||
Connect Wallet
|
||||
</span>
|
||||
</div>
|
||||
<Button size="sm" onClick={handleConnectWallet}>
|
||||
Connect Wallet
|
||||
@ -274,17 +328,23 @@ export function ConnectStep() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{missing.githubBackendSync && !missing.walletConnection && !missing.clerkGithub && (
|
||||
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Github className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Sync GitHub Access</span>
|
||||
</div>
|
||||
<GitHubBackendAuth onAuthStatusChange={handleGithubAuthChange} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{missing.githubBackendSync &&
|
||||
!missing.walletConnection &&
|
||||
!missing.clerkGithub && (
|
||||
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Github className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">
|
||||
Sync GitHub Access
|
||||
</span>
|
||||
</div>
|
||||
<GitHubBackendAuth
|
||||
onAuthStatusChange={handleGithubAuthChange}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
@ -292,20 +352,22 @@ export function ConnectStep() {
|
||||
{/* Mode Selection Tabs */}
|
||||
<div className="grid grid-cols-2 gap-1 p-1 bg-zinc-100 dark:bg-zinc-800 rounded-lg mb-6">
|
||||
<Button
|
||||
variant={isImportMode ? "default" : "ghost"}
|
||||
className={`${isImportMode
|
||||
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
||||
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
||||
variant={isImportMode ? 'default' : 'ghost'}
|
||||
className={`${
|
||||
isImportMode
|
||||
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
||||
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
||||
}`}
|
||||
onClick={() => toggleMode('import')}
|
||||
>
|
||||
Import a repository
|
||||
</Button>
|
||||
<Button
|
||||
variant={!isImportMode ? "default" : "ghost"}
|
||||
className={`${!isImportMode
|
||||
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
||||
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
||||
variant={!isImportMode ? 'default' : 'ghost'}
|
||||
className={`${
|
||||
!isImportMode
|
||||
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
||||
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
||||
}`}
|
||||
onClick={() => toggleMode('template')}
|
||||
>
|
||||
@ -327,7 +389,8 @@ export function ConnectStep() {
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
No repositories found. Make sure your GitHub account has repositories.
|
||||
No repositories found. Make sure your GitHub account has
|
||||
repositories.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@ -346,9 +409,13 @@ export function ConnectStep() {
|
||||
>
|
||||
<Github className="h-5 w-5 mr-3 text-zinc-500 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm">{repo.full_name}</div>
|
||||
<div className="font-medium text-sm">
|
||||
{repo.full_name}
|
||||
</div>
|
||||
{repo.description && (
|
||||
<div className="text-xs text-zinc-500 truncate">{repo.description}</div>
|
||||
<div className="text-xs text-zinc-500 truncate">
|
||||
{repo.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedRepo === repo.full_name && (
|
||||
@ -361,7 +428,10 @@ export function ConnectStep() {
|
||||
{/* Project Name Input for Repository Import */}
|
||||
{selectedRepo && (
|
||||
<div className="mt-6 space-y-2">
|
||||
<Label htmlFor="projectName" className="text-sm font-medium">
|
||||
<Label
|
||||
htmlFor="projectName"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Project Name
|
||||
</Label>
|
||||
<Input
|
||||
@ -382,38 +452,46 @@ export function ConnectStep() {
|
||||
) : (
|
||||
/* Template Selection */
|
||||
<div className="space-y-4">
|
||||
{AVAILABLE_TEMPLATES.filter(t => !t.isComingSoon).map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
className={`flex items-center p-4 rounded-lg border cursor-pointer transition-all ${
|
||||
selectedTemplate?.id === template.id
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-950/20'
|
||||
: 'border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600'
|
||||
}`}
|
||||
onClick={() => handleTemplateSelect(template)}
|
||||
>
|
||||
{/* 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="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' ? 'PWA' : template.icon === 'nextjs' ? 'N' : 'IMG'}
|
||||
{AVAILABLE_TEMPLATES.filter((t) => !t.isComingSoon).map(
|
||||
(template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
className={`flex items-center p-4 rounded-lg border cursor-pointer transition-all ${
|
||||
selectedTemplate?.id === template.id
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-950/20'
|
||||
: 'border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600'
|
||||
}`}
|
||||
onClick={() => handleTemplateSelect(template)}
|
||||
>
|
||||
{/* 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="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'
|
||||
? 'PWA'
|
||||
: template.icon === 'nextjs'
|
||||
? 'N'
|
||||
: 'IMG'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Template Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm mb-1">{template.name}</div>
|
||||
<div className="flex items-center text-xs text-zinc-500">
|
||||
<Github className="h-3 w-3 mr-1" />
|
||||
{template.repoFullName}
|
||||
{/* Template Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm mb-1">
|
||||
{template.name}
|
||||
</div>
|
||||
<div className="flex items-center text-xs text-zinc-500">
|
||||
<Github className="h-3 w-3 mr-1" />
|
||||
{template.repoFullName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selection Indicator */}
|
||||
{selectedTemplate?.id === template.id && (
|
||||
<CheckCircle2 className="h-5 w-5 text-blue-500 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{/* Selection Indicator */}
|
||||
{selectedTemplate?.id === template.id && (
|
||||
<CheckCircle2 className="h-5 w-5 text-blue-500 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Project Name Input for Templates */}
|
||||
{selectedTemplate && (
|
||||
@ -443,7 +521,12 @@ export function ConnectStep() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={!isFullyAuthenticated || (isImportMode ? !selectedRepo : (!selectedTemplate || !projectName.trim()))}
|
||||
disabled={
|
||||
!isFullyAuthenticated ||
|
||||
(isImportMode
|
||||
? !selectedRepo
|
||||
: !selectedTemplate || !projectName.trim())
|
||||
}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
@ -1,19 +1,30 @@
|
||||
// src/components/onboarding/deploy-step/deploy-step.tsx
|
||||
'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/store'
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { useWallet } from '@/context/WalletContext'
|
||||
import { useDeployment } from '@/hooks/useDeployment'
|
||||
import { useTemplateDeployment } from '@/hooks/useTemplate'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Progress } from '@workspace/ui/components/progress'
|
||||
import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogFooter } from '@workspace/ui/components/dialog'
|
||||
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||
import { Badge } from '@workspace/ui/components/badge'
|
||||
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 { AlertTriangle, CheckCircle2, Github, Loader2 } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function DeployStep() {
|
||||
@ -29,7 +40,8 @@ export function DeployStep() {
|
||||
// Contexts and hooks
|
||||
const { wallet } = useWallet()
|
||||
const { deployRepository, isDeploying: isRepoDeploying } = useDeployment()
|
||||
const { deployTemplate, isDeploying: isTemplateDeploying } = useTemplateDeployment()
|
||||
const { deployTemplate, isDeploying: isTemplateDeploying } =
|
||||
useTemplateDeployment()
|
||||
|
||||
// Determine deployment type and get the right deploying state
|
||||
const isTemplateMode = formData.deploymentMode === 'template'
|
||||
@ -53,7 +65,10 @@ export function DeployStep() {
|
||||
return {
|
||||
name: formData.githubRepo?.split('/').pop() || 'Repository',
|
||||
source: formData.githubRepo || 'Unknown Repository',
|
||||
projectName: formData.projectName || formData.githubRepo?.split('/').pop() || 'New Project',
|
||||
projectName:
|
||||
formData.projectName ||
|
||||
formData.githubRepo?.split('/').pop() ||
|
||||
'New Project',
|
||||
type: 'Repository'
|
||||
}
|
||||
}
|
||||
@ -105,7 +120,9 @@ export function DeployStep() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Deployment failed:', error)
|
||||
setDeploymentError(error instanceof Error ? error.message : 'Deployment failed')
|
||||
setDeploymentError(
|
||||
error instanceof Error ? error.message : 'Deployment failed'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,8 +142,8 @@ export function DeployStep() {
|
||||
organizationSlug: formData.selectedOrg,
|
||||
environmentVariables: formData.environmentVariables || [],
|
||||
deployerLrn: formData.selectedLrn
|
||||
};
|
||||
const result = await deployTemplate(config);
|
||||
}
|
||||
const result = await deployTemplate(config)
|
||||
|
||||
// Save deployment results
|
||||
setFormData({
|
||||
@ -156,7 +173,10 @@ export function DeployStep() {
|
||||
organizationSlug: formData.selectedOrg,
|
||||
repository: formData.githubRepo,
|
||||
branch: 'main', // Default branch
|
||||
name: formData.projectName || formData.githubRepo.split('/').pop() || 'New Project',
|
||||
name:
|
||||
formData.projectName ||
|
||||
formData.githubRepo.split('/').pop() ||
|
||||
'New Project',
|
||||
environmentVariables: formData.environmentVariables || []
|
||||
}
|
||||
|
||||
@ -194,18 +214,66 @@ export function DeployStep() {
|
||||
<div className="max-w-md w-full mx-auto">
|
||||
{/* Deploy icon */}
|
||||
<div className="mx-auto mb-6 flex justify-center">
|
||||
<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"}>
|
||||
<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
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={isDarkMode ? 'text-white' : 'text-black'}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Deploy header */}
|
||||
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>
|
||||
<h2
|
||||
className={`text-2xl font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} text-center mb-2`}
|
||||
>
|
||||
{isDeploying ? 'Deploying...' : 'Deploy'}
|
||||
</h2>
|
||||
<p className="text-center text-zinc-500 mb-8">
|
||||
@ -226,7 +294,9 @@ export function DeployStep() {
|
||||
<div className="flex items-center gap-3">
|
||||
<Github className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm">{deploymentInfo.projectName}</div>
|
||||
<div className="font-medium text-sm">
|
||||
{deploymentInfo.projectName}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground font-mono">
|
||||
{deploymentInfo.source}
|
||||
</div>
|
||||
@ -240,18 +310,25 @@ export function DeployStep() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">Deployer</div>
|
||||
<div className="font-medium">{formData.selectedLrn ? 'LRN' : 'Auction'}</div>
|
||||
<div className="font-medium">
|
||||
{formData.selectedLrn ? 'LRN' : 'Auction'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.environmentVariables && formData.environmentVariables.length > 0 && (
|
||||
<div className="pt-2">
|
||||
<div className="text-xs text-muted-foreground mb-1">Environment Variables</div>
|
||||
<div className="text-xs">
|
||||
{formData.environmentVariables.length} variable{formData.environmentVariables.length !== 1 ? 's' : ''} configured
|
||||
{formData.environmentVariables &&
|
||||
formData.environmentVariables.length > 0 && (
|
||||
<div className="pt-2">
|
||||
<div className="text-xs text-muted-foreground mb-1">
|
||||
Environment Variables
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
{formData.environmentVariables.length} variable
|
||||
{formData.environmentVariables.length !== 1 ? 's' : ''}{' '}
|
||||
configured
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -273,7 +350,8 @@ export function DeployStep() {
|
||||
<AlertDescription>
|
||||
<div className="font-medium">Deployment Successful!</div>
|
||||
<div className="text-sm mt-1">
|
||||
Your project has been deployed successfully. You'll be redirected to the project dashboard.
|
||||
Your project has been deployed successfully. You'll be
|
||||
redirected to the project dashboard.
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@ -283,13 +361,21 @@ export function DeployStep() {
|
||||
{isDeploying && (
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className={`${isDarkMode ? "text-white" : "text-zinc-900"} text-sm`}>
|
||||
{isTemplateMode ? 'Creating repository from template...' : 'Deploying repository...'}
|
||||
<div
|
||||
className={`${isDarkMode ? 'text-white' : 'text-zinc-900'} text-sm`}
|
||||
>
|
||||
{isTemplateMode
|
||||
? 'Creating repository from template...'
|
||||
: 'Deploying repository...'}
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={undefined} className={`h-2 ${isDarkMode ? "bg-zinc-800" : "bg-zinc-200"}`} />
|
||||
<Progress
|
||||
value={undefined}
|
||||
className={`h-2 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'}`}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
This process may take several minutes. Please do not close this window.
|
||||
This process may take several minutes. Please do not close this
|
||||
window.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -298,7 +384,7 @@ export function DeployStep() {
|
||||
<div className="flex justify-between items-center mt-4">
|
||||
<Button
|
||||
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}
|
||||
disabled={isDeploying || deploymentSuccess}
|
||||
>
|
||||
@ -315,7 +401,7 @@ export function DeployStep() {
|
||||
</Button>
|
||||
) : isDeploying ? (
|
||||
<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
|
||||
>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
@ -327,9 +413,22 @@ export function DeployStep() {
|
||||
onClick={handlePayAndDeploy}
|
||||
disabled={deploymentError !== null}
|
||||
>
|
||||
{formData.deploymentType === 'auction' ? 'Pay and Deploy' : 'Deploy'}
|
||||
<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"/>
|
||||
{formData.deploymentType === 'auction'
|
||||
? 'Pay and Deploy'
|
||||
: 'Deploy'}
|
||||
<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>
|
||||
</Button>
|
||||
)}
|
||||
@ -360,7 +459,9 @@ export function DeployStep() {
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Source:</span>
|
||||
<span className="font-mono text-xs">{deploymentInfo.source}</span>
|
||||
<span className="font-mono text-xs">
|
||||
{deploymentInfo.source}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -378,7 +479,9 @@ export function DeployStep() {
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Deployer</h3>
|
||||
<div className="text-sm">
|
||||
<div className="font-mono text-xs">{formData.selectedLrn}</div>
|
||||
<div className="font-mono text-xs">
|
||||
{formData.selectedLrn}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -402,17 +505,10 @@ export function DeployStep() {
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-end space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancelConfirm}
|
||||
>
|
||||
<Button variant="outline" onClick={handleCancelConfirm}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirmDeploy}
|
||||
>
|
||||
Confirm Deployment
|
||||
</Button>
|
||||
<Button onClick={handleConfirmDeploy}>Confirm Deployment</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@ -10,9 +10,7 @@
|
||||
|
||||
// Main component
|
||||
export { default as Onboarding } from './Onboarding'
|
||||
export {
|
||||
default as OnboardingDialog,
|
||||
} from './OnboardingDialog'
|
||||
export { default as OnboardingDialog } from './OnboardingDialog'
|
||||
|
||||
// Step components
|
||||
export { ConfigureStep } from './configure-step'
|
||||
@ -27,7 +25,7 @@ export * from './common'
|
||||
export * from './sidebar'
|
||||
|
||||
// Store and hooks
|
||||
export { useOnboarding } from './store'
|
||||
export { useOnboarding } from './useOnboarding'
|
||||
|
||||
// Types
|
||||
export * from './types'
|
||||
|
@ -18,6 +18,7 @@ import type { OnboardingFormData, Step } from './types'
|
||||
* @property {(data: Partial<OnboardingFormData>) => void} setFormData - Updates form data
|
||||
* @property {() => void} nextStep - Moves to the next step
|
||||
* @property {() => void} previousStep - Moves to the previous step
|
||||
* @property {() => void} resetOnboarding - Resets the onboarding state to initial values
|
||||
*/
|
||||
export interface OnboardingState {
|
||||
currentStep: Step
|
||||
@ -26,11 +27,34 @@ export interface OnboardingState {
|
||||
setFormData: (data: Partial<OnboardingFormData>) => void
|
||||
nextStep: () => void
|
||||
previousStep: () => void
|
||||
resetOnboarding: () => void
|
||||
}
|
||||
|
||||
/** Order of steps in the onboarding flow */
|
||||
const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy']
|
||||
|
||||
/** Initial form data values */
|
||||
const initialFormData: OnboardingFormData = {
|
||||
projectName: '',
|
||||
repoName: '',
|
||||
repoDescription: '',
|
||||
framework: '',
|
||||
access: 'public',
|
||||
organizationSlug: '',
|
||||
template: undefined,
|
||||
githubRepo: '',
|
||||
selectedOrg: '',
|
||||
environmentVariables: [],
|
||||
selectedLrn: '',
|
||||
deploymentType: 'lrn',
|
||||
maxPrice: '1000',
|
||||
deployerCount: '1',
|
||||
deploymentId: undefined,
|
||||
deploymentUrl: undefined,
|
||||
projectId: undefined,
|
||||
repositoryUrl: undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Zustand store for managing onboarding state
|
||||
* Used across all onboarding components to maintain flow state
|
||||
@ -42,14 +66,7 @@ const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy']
|
||||
*/
|
||||
export const useOnboarding = create<OnboardingState>((set) => ({
|
||||
currentStep: 'connect',
|
||||
formData: {
|
||||
projectName: '',
|
||||
repoName: '',
|
||||
repoDescription: '',
|
||||
framework: '',
|
||||
access: 'public',
|
||||
organizationSlug: ''
|
||||
},
|
||||
formData: initialFormData,
|
||||
setCurrentStep: (step) => set({ currentStep: step }),
|
||||
setFormData: (data) =>
|
||||
set((state) => ({
|
||||
@ -66,5 +83,6 @@ export const useOnboarding = create<OnboardingState>((set) => ({
|
||||
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
|
||||
const previousStep = STEP_ORDER[currentIndex - 1]
|
||||
return previousStep ? { currentStep: previousStep } : state
|
||||
})
|
||||
}),
|
||||
resetOnboarding: () => set({ currentStep: 'connect', formData: initialFormData })
|
||||
}))
|
||||
|
@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { CheckCircle } from 'lucide-react'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { CheckCircle } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function SuccessStep() {
|
||||
const router = useRouter()
|
||||
@ -20,10 +20,14 @@ export function SuccessStep() {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
// Get deployment info from form data
|
||||
const repoName = formData.githubRepo ? formData.githubRepo.split('/').pop() : (formData.projectName || 'project')
|
||||
const deploymentUrl = formData.deploymentUrl || `https://${repoName}.laconic.deploy`
|
||||
const projectId = formData.projectId || 'unknown-id'
|
||||
// Get deployment info from form data - using available properties
|
||||
const repoName = formData.githubRepo
|
||||
? formData.githubRepo.split('/').pop()
|
||||
: formData.repoName
|
||||
? formData.repoName.split('/').pop()
|
||||
: formData.projectName || 'project'
|
||||
const deploymentUrl = `https://${repoName}.laconic.deploy` // Default deployment URL
|
||||
const projectId = formData.projectId || formData.deploymentId || 'unknown-id' // Use projectId first, fallback to deploymentId
|
||||
|
||||
// Handle "View Project" button - navigates to project page
|
||||
const handleViewProject = () => {
|
||||
@ -62,7 +66,9 @@ export function SuccessStep() {
|
||||
</div>
|
||||
|
||||
{/* Success header */}
|
||||
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>
|
||||
<h2
|
||||
className={`text-2xl font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} text-center mb-2`}
|
||||
>
|
||||
Successfully Deployed!
|
||||
</h2>
|
||||
<p className="text-center text-zinc-500 mb-8">
|
||||
@ -70,7 +76,9 @@ export function SuccessStep() {
|
||||
</p>
|
||||
|
||||
{/* Deployment summary */}
|
||||
<div className={`border rounded-md p-4 mb-6 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
|
||||
<div
|
||||
className={`border rounded-md p-4 mb-6 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Project:</span>
|
||||
@ -89,17 +97,49 @@ export function SuccessStep() {
|
||||
|
||||
{/* Next steps section */}
|
||||
<div className="mb-8">
|
||||
<h3 className={`text-lg font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-4`}>Next steps</h3>
|
||||
<h3
|
||||
className={`text-lg font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} mb-4`}
|
||||
>
|
||||
Next steps
|
||||
</h3>
|
||||
|
||||
<div className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
|
||||
<div
|
||||
className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||
>
|
||||
<div className="flex items-center p-4 justify-between">
|
||||
<div>
|
||||
<div className={isDarkMode ? "text-white font-medium" : "text-zinc-900 font-medium"}>Setup Domain</div>
|
||||
<div className="text-zinc-500 text-sm">Add a custom domain to your project.</div>
|
||||
<div
|
||||
className={
|
||||
isDarkMode
|
||||
? 'text-white font-medium'
|
||||
: 'text-zinc-900 font-medium'
|
||||
}
|
||||
>
|
||||
Setup Domain
|
||||
</div>
|
||||
<div className="text-zinc-500 text-sm">
|
||||
Add a custom domain to your project.
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className={`rounded-full p-1 w-8 h-8 flex items-center justify-center ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={isDarkMode ? "text-white" : "text-zinc-900"}>
|
||||
<path d="M9 18L15 12L9 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`rounded-full p-1 w-8 h-8 flex items-center justify-center ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={isDarkMode ? 'text-white' : 'text-zinc-900'}
|
||||
>
|
||||
<path
|
||||
d="M9 18L15 12L9 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
@ -114,8 +154,19 @@ export function SuccessStep() {
|
||||
disabled={!projectId || projectId === 'unknown-id'}
|
||||
>
|
||||
View Project
|
||||
<svg className="ml-2 h-4 w-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 12H19M19 12L13 6M19 12L13 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<svg
|
||||
className="ml-2 h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 12H19M19 12L13 6M19 12L13 18"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
/**
|
||||
* Available steps in the onboarding flow
|
||||
*/
|
||||
export type Step = 'connect' | 'configure' | 'deploy'
|
||||
export type Step = 'connect' | 'configure' | 'deploy' | 'success'
|
||||
|
||||
/**
|
||||
* Form data collected during the onboarding process
|
||||
@ -18,6 +18,18 @@ export type Step = 'connect' | 'configure' | 'deploy'
|
||||
* @property {string} framework - Framework used for the project
|
||||
* @property {string} access - Access level of the repository
|
||||
* @property {string} organizationSlug - Organization slug
|
||||
* @property {Template | undefined} template - Selected template for deployment
|
||||
* @property {string} githubRepo - GitHub repository path (owner/repo)
|
||||
* @property {string} selectedOrg - Selected organization for deployment
|
||||
* @property {EnvironmentVariable[]} environmentVariables - Environment variables for deployment
|
||||
* @property {string} selectedLrn - Selected LRN for deployment
|
||||
* @property {string} deploymentType - Type of deployment (lrn, wallet, etc.)
|
||||
* @property {string} maxPrice - Maximum price for deployment
|
||||
* @property {string} deployerCount - Number of deployers
|
||||
* @property {string} deploymentId - ID of the deployment after creation
|
||||
* @property {string} deploymentUrl - URL of the deployed project
|
||||
* @property {string} projectId - ID of the created project
|
||||
* @property {string} repositoryUrl - URL of the repository
|
||||
*/
|
||||
export interface OnboardingFormData {
|
||||
projectName: string
|
||||
@ -26,6 +38,18 @@ export interface OnboardingFormData {
|
||||
framework: string
|
||||
access: 'public' | 'private'
|
||||
organizationSlug: string
|
||||
template?: Template
|
||||
githubRepo: string
|
||||
selectedOrg: string
|
||||
environmentVariables: EnvironmentVariable[]
|
||||
selectedLrn: string
|
||||
deploymentType: string
|
||||
maxPrice: string
|
||||
deployerCount: string
|
||||
deploymentId?: string
|
||||
deploymentUrl?: string
|
||||
projectId?: string
|
||||
repositoryUrl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,12 +78,16 @@ export interface Repository {
|
||||
* @property {string} name - Template name
|
||||
* @property {string} [description] - Template description
|
||||
* @property {string} [thumbnail] - Template thumbnail URL
|
||||
* @property {string} [repoFullName] - Full repository name for the template
|
||||
* @property {any} [icon] - Template icon
|
||||
*/
|
||||
export interface Template {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
thumbnail?: string
|
||||
repoFullName?: string
|
||||
icon?: any
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,9 +109,11 @@ export interface DeploymentType {
|
||||
* @property {string} key - Environment variable key
|
||||
* @property {string} value - Environment variable value
|
||||
* @property {boolean} [isSecret] - Whether the variable is a secret
|
||||
* @property {string[]} environments - Environment names where this variable applies
|
||||
*/
|
||||
export interface EnvironmentVariable {
|
||||
key: string
|
||||
value: string
|
||||
isSecret?: boolean
|
||||
environments: string[]
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const initGQLClient = async () => {
|
||||
try {
|
||||
const client = new GQLClient({
|
||||
gqlEndpoint: 'http://localhost:8000/graphql',
|
||||
gqlEndpoint: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'}/graphql`,
|
||||
})
|
||||
setGqlClient(client)
|
||||
} catch (error) {
|
||||
|
@ -11,6 +11,8 @@ import {
|
||||
useCallback
|
||||
} from 'react'
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
||||
|
||||
/**
|
||||
* @interface BackendContextType
|
||||
* @description Defines the structure of the BackendContext value.
|
||||
@ -45,7 +47,7 @@ export const BackendProvider: React.FC<{ children: ReactNode }> = ({
|
||||
// Check backend connection
|
||||
const checkBackendConnection = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8000/auth/session', {
|
||||
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
})
|
||||
|
@ -1,11 +1,11 @@
|
||||
// src/hooks/useAuthStatus.tsx
|
||||
'use client'
|
||||
|
||||
import { useAuth, useUser } from '@clerk/nextjs'
|
||||
import { useWallet } from '@/context/WalletContext' // Use the full provider!
|
||||
import { useBackend } from '@/context/BackendContext'
|
||||
import { useGQLClient } from '@/context'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useBackend } from '@/context/BackendContext'
|
||||
import { useWallet } from '@/context/WalletContext' // Use the full provider!
|
||||
import { useAuth, useUser } from '@clerk/nextjs'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* @interface AuthStatus
|
||||
@ -20,8 +20,8 @@ export interface AuthStatus {
|
||||
user: any
|
||||
}
|
||||
wallet: {
|
||||
isConnected: boolean // SIWE authenticated + backend session
|
||||
hasAddress: boolean // Just has wallet address
|
||||
isConnected: boolean // SIWE authenticated + backend session
|
||||
hasAddress: boolean // Just has wallet address
|
||||
wallet: any
|
||||
}
|
||||
backend: {
|
||||
@ -76,7 +76,7 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
|
||||
// Wallet authentication
|
||||
const {
|
||||
isConnected: isWalletSessionActive, // SIWE authenticated
|
||||
isConnected: isWalletSessionActive, // SIWE authenticated
|
||||
hasWalletAddress,
|
||||
wallet,
|
||||
connect: connectWallet
|
||||
@ -95,13 +95,30 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
// GitHub backend auth state
|
||||
const [isGithubBackendAuth, setIsGithubBackendAuth] = useState(false)
|
||||
const [isCheckingGithubAuth, setIsCheckingGithubAuth] = useState(false)
|
||||
const lastGithubCheckRef = useRef(0)
|
||||
const isCheckingRef = useRef(false)
|
||||
|
||||
// Stable status to prevent rapid UI changes
|
||||
const [stableAuthStatus, setStableAuthStatus] = useState({
|
||||
isFullyAuthenticated: false,
|
||||
lastUpdate: 0
|
||||
})
|
||||
|
||||
// Check GitHub backend auth via GraphQL
|
||||
const checkGithubBackendAuth = useCallback(async (): Promise<boolean> => {
|
||||
if (!isBackendConnected) return false
|
||||
|
||||
// Prevent multiple rapid calls - only allow once every 3 seconds
|
||||
const now = Date.now()
|
||||
if (isCheckingRef.current || (now - lastGithubCheckRef.current < 3000)) {
|
||||
return isGithubBackendAuth
|
||||
}
|
||||
|
||||
try {
|
||||
isCheckingRef.current = true
|
||||
setIsCheckingGithubAuth(true)
|
||||
lastGithubCheckRef.current = now
|
||||
|
||||
const userData = await gqlClient.getUser()
|
||||
const hasGitHubToken = !!userData.user.gitHubToken
|
||||
setIsGithubBackendAuth(hasGitHubToken)
|
||||
@ -111,9 +128,10 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
setIsGithubBackendAuth(false)
|
||||
return false
|
||||
} finally {
|
||||
isCheckingRef.current = false
|
||||
setIsCheckingGithubAuth(false)
|
||||
}
|
||||
}, [isBackendConnected, gqlClient])
|
||||
}, [isBackendConnected, isGithubBackendAuth]) // Minimal dependencies
|
||||
|
||||
// Check GitHub auth when backend connection changes
|
||||
useEffect(() => {
|
||||
@ -122,7 +140,7 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
} else {
|
||||
setIsGithubBackendAuth(false)
|
||||
}
|
||||
}, [isBackendConnected, checkGithubBackendAuth])
|
||||
}, [isBackendConnected]) // Remove checkGithubBackendAuth from dependencies to prevent infinite loop
|
||||
|
||||
// Check backend connection when wallet session is active (SIWE completed)
|
||||
useEffect(() => {
|
||||
@ -133,12 +151,15 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
}, 1000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isWalletSessionActive, refreshBackendStatus])
|
||||
}, [isWalletSessionActive]) // Remove refreshBackendStatus from dependencies to prevent rapid retriggers
|
||||
|
||||
// Check if GitHub is connected in Clerk
|
||||
const hasGithubInClerk = user?.externalAccounts?.find(
|
||||
account => account.provider === 'github' || account.verification?.strategy === 'oauth_github'
|
||||
) !== undefined
|
||||
const hasGithubInClerk =
|
||||
user?.externalAccounts?.find(
|
||||
(account) =>
|
||||
account.provider === 'github' ||
|
||||
account.verification?.strategy === 'oauth_github'
|
||||
) !== undefined
|
||||
|
||||
// Calculate what's missing
|
||||
const missing = {
|
||||
@ -151,11 +172,11 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
|
||||
// Calculate progress
|
||||
const authSteps = [
|
||||
isSignedIn, // Clerk sign in
|
||||
hasGithubInClerk, // GitHub connected to Clerk
|
||||
hasWalletAddress, // Wallet address obtained
|
||||
isWalletSessionActive, // SIWE authentication completed
|
||||
isGithubBackendAuth // GitHub synced to backend
|
||||
isSignedIn, // Clerk sign in
|
||||
hasGithubInClerk, // GitHub connected to Clerk
|
||||
hasWalletAddress, // Wallet address obtained
|
||||
isWalletSessionActive, // SIWE authentication completed
|
||||
isGithubBackendAuth // GitHub synced to backend
|
||||
]
|
||||
|
||||
const completedSteps = authSteps.filter(Boolean).length
|
||||
@ -163,10 +184,28 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
const progressPercentage = Math.round((completedSteps / totalSteps) * 100)
|
||||
|
||||
// Determine if fully authenticated
|
||||
const isFullyAuthenticated = authSteps.every(Boolean)
|
||||
const currentIsFullyAuthenticated = authSteps.every(Boolean)
|
||||
|
||||
// Debounce authentication status changes to prevent flickering
|
||||
useEffect(() => {
|
||||
const now = Date.now()
|
||||
const timeSinceLastUpdate = now - stableAuthStatus.lastUpdate
|
||||
|
||||
// Only update if status actually changed and enough time has passed (300ms debounce)
|
||||
if (currentIsFullyAuthenticated !== stableAuthStatus.isFullyAuthenticated && timeSinceLastUpdate > 300) {
|
||||
setStableAuthStatus({
|
||||
isFullyAuthenticated: currentIsFullyAuthenticated,
|
||||
lastUpdate: now
|
||||
})
|
||||
}
|
||||
}, [currentIsFullyAuthenticated, stableAuthStatus])
|
||||
|
||||
// Use stable status for UI
|
||||
const isFullyAuthenticated = stableAuthStatus.isFullyAuthenticated
|
||||
|
||||
// Determine if ready (all auth systems loaded)
|
||||
const isReady = isClerkLoaded && isUserLoaded && !isBackendLoading && !isCheckingGithubAuth
|
||||
const isReady =
|
||||
isClerkLoaded && isUserLoaded && !isBackendLoading && !isCheckingGithubAuth
|
||||
|
||||
// Combined refresh action
|
||||
const refreshAllStatus = async () => {
|
||||
|
118
apps/deploy-fe/src/hooks/useDirectGitHub.tsx
Normal file
118
apps/deploy-fe/src/hooks/useDirectGitHub.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
'use client'
|
||||
|
||||
import { getGitHubToken } from '@/actions/github'
|
||||
import { Octokit } from '@octokit/rest'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface CreateRepoFromTemplateParams {
|
||||
templateOwner: string
|
||||
templateRepo: string
|
||||
name: string
|
||||
description?: string
|
||||
isPrivate?: boolean
|
||||
}
|
||||
|
||||
interface CreateRepoResult {
|
||||
success: boolean
|
||||
repositoryUrl?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to directly interact with GitHub API using user's own token
|
||||
* Bypasses backend GitHub integration issues
|
||||
*/
|
||||
export function useDirectGitHub() {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const createRepoFromTemplate = async (
|
||||
params: CreateRepoFromTemplateParams
|
||||
): Promise<CreateRepoResult> => {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
console.log(
|
||||
'🔄 Creating repository from template directly via GitHub API...'
|
||||
)
|
||||
console.log('📋 Parameters:', params)
|
||||
|
||||
// Get user's GitHub token
|
||||
const token = await getGitHubToken()
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
'GitHub token not available. Please reconnect your GitHub account.'
|
||||
)
|
||||
}
|
||||
|
||||
// Create Octokit instance with user's token
|
||||
const octokit = new Octokit({ auth: token })
|
||||
|
||||
// Get the authenticated user's info to use as owner
|
||||
const { data: authUser } = await octokit.rest.users.getAuthenticated()
|
||||
console.log('👤 Authenticated GitHub user:', authUser.login)
|
||||
|
||||
// Create repository from template
|
||||
const { data: newRepo } = await octokit.rest.repos.createUsingTemplate({
|
||||
template_owner: params.templateOwner,
|
||||
template_repo: params.templateRepo,
|
||||
owner: authUser.login, // Use authenticated user as owner
|
||||
name: params.name,
|
||||
description:
|
||||
params.description ||
|
||||
`Created from ${params.templateOwner}/${params.templateRepo}`,
|
||||
private: params.isPrivate || false,
|
||||
include_all_branches: false
|
||||
})
|
||||
|
||||
console.log('✅ Repository created successfully:', newRepo.html_url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
repositoryUrl: newRepo.html_url
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create repository from template:', error)
|
||||
|
||||
let errorMessage = 'Failed to create repository from template'
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getUserRepos = async () => {
|
||||
try {
|
||||
const token = await getGitHubToken()
|
||||
if (!token) {
|
||||
throw new Error('GitHub token not available')
|
||||
}
|
||||
|
||||
const octokit = new Octokit({ auth: token })
|
||||
const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser(
|
||||
{
|
||||
sort: 'updated',
|
||||
per_page: 100
|
||||
}
|
||||
)
|
||||
|
||||
return repos
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch user repositories:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createRepoFromTemplate,
|
||||
getUserRepos,
|
||||
isLoading
|
||||
}
|
||||
}
|
316
apps/deploy-fe/src/hooks/useDirectTemplateDeployment.tsx
Normal file
316
apps/deploy-fe/src/hooks/useDirectTemplateDeployment.tsx
Normal file
@ -0,0 +1,316 @@
|
||||
// src/hooks/useDirectTemplateDeployment.tsx
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useGQLClient } from '@/context'
|
||||
import { useWallet } from '@/context/WalletContext'
|
||||
import { useUser } from '@clerk/nextjs'
|
||||
import { toast } from 'sonner'
|
||||
import type { TemplateDetail } from '@/constants/templates'
|
||||
import { useDirectGitHub } from './useDirectGitHub'
|
||||
import { getGitHubToken } from '@/actions/github'
|
||||
|
||||
export interface TemplateDeploymentConfig {
|
||||
template: TemplateDetail
|
||||
projectName: string
|
||||
organizationSlug: string
|
||||
environmentVariables?: Array<{
|
||||
key: string
|
||||
value: string
|
||||
environments: string[]
|
||||
}>
|
||||
deployerLrn?: string
|
||||
}
|
||||
|
||||
export interface TemplateDeploymentResult {
|
||||
projectId: string
|
||||
repositoryUrl: string
|
||||
deploymentUrl?: string
|
||||
deploymentId?: string
|
||||
}
|
||||
|
||||
export function useDirectTemplateDeployment() {
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const [deploymentResult, setDeploymentResult] =
|
||||
useState<TemplateDeploymentResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const gqlClient = useGQLClient()
|
||||
const { wallet } = useWallet()
|
||||
const { user } = useUser()
|
||||
const directGitHub = useDirectGitHub()
|
||||
|
||||
const deployTemplate = async (
|
||||
config: TemplateDeploymentConfig
|
||||
): Promise<TemplateDeploymentResult> => {
|
||||
setIsDeploying(true)
|
||||
setError(null)
|
||||
setDeploymentResult(null)
|
||||
|
||||
try {
|
||||
console.log('🚀 Starting direct template deployment:', config)
|
||||
|
||||
// Validate required data
|
||||
if (!wallet?.address) {
|
||||
throw new Error('Wallet not connected')
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not authenticated')
|
||||
}
|
||||
|
||||
// Get GitHub username from Clerk external accounts
|
||||
const githubAccount = user.externalAccounts?.find(
|
||||
(account) => account.provider === 'github'
|
||||
)
|
||||
const githubUsername = githubAccount?.username
|
||||
|
||||
if (!githubUsername) {
|
||||
throw new Error('GitHub account not connected')
|
||||
}
|
||||
|
||||
console.log('🔍 GitHub user info:', {
|
||||
githubUsername,
|
||||
githubAccount: githubAccount?.username,
|
||||
userExternalAccounts: user.externalAccounts?.length
|
||||
})
|
||||
|
||||
// Parse template repository (format: "owner/repo")
|
||||
const [templateOwner, templateRepo] =
|
||||
config.template.repoFullName.split('/')
|
||||
if (!templateOwner || !templateRepo) {
|
||||
throw new Error('Invalid template repository format')
|
||||
}
|
||||
|
||||
console.log('🔍 Template parsing details:', {
|
||||
originalTemplate: config.template.repoFullName,
|
||||
parsedOwner: templateOwner,
|
||||
parsedRepo: templateRepo,
|
||||
templateId: config.template.id,
|
||||
templateName: config.template.name
|
||||
})
|
||||
|
||||
toast.info('Creating repository from template...')
|
||||
|
||||
// STEP 1: Create repository directly via GitHub API with user's token
|
||||
const repoResult = await directGitHub.createRepoFromTemplate({
|
||||
templateOwner,
|
||||
templateRepo,
|
||||
name: config.projectName,
|
||||
description: `Created from ${config.template.name} template`,
|
||||
isPrivate: false
|
||||
})
|
||||
|
||||
if (!repoResult.success || !repoResult.repositoryUrl) {
|
||||
throw new Error(
|
||||
repoResult.error || 'Failed to create repository from template'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(
|
||||
'✅ Repository created successfully:',
|
||||
repoResult.repositoryUrl
|
||||
)
|
||||
toast.success('Repository created from template!')
|
||||
|
||||
// STEP 2: Create project in backend using the newly created repository
|
||||
console.log('🔍 Preparing backend project creation...')
|
||||
console.log('🔍 Organization slug:', config.organizationSlug)
|
||||
console.log('🔍 Deployer LRN:', config.deployerLrn || 'undefined')
|
||||
console.log(
|
||||
'🔍 Environment variables:',
|
||||
config.environmentVariables || []
|
||||
)
|
||||
|
||||
toast.info('Setting up project deployment...')
|
||||
|
||||
// Add a delay to ensure the repository is fully created and accessible
|
||||
console.log('⏳ Waiting for repository to be fully accessible...')
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
|
||||
// STEP 2a: Verify GitHub token availability
|
||||
console.log('🔄 Verifying GitHub token for user:', githubUsername)
|
||||
const currentClerkToken = await getGitHubToken()
|
||||
if (!currentClerkToken) {
|
||||
throw new Error(
|
||||
`GitHub token not available for user ${githubUsername}. Please reconnect your GitHub account.`
|
||||
)
|
||||
}
|
||||
console.log('✅ GitHub token verified for user:', githubUsername)
|
||||
|
||||
try {
|
||||
// Get available deployers
|
||||
console.log('🔍 Fetching available deployers...')
|
||||
const deployersResult = await gqlClient.getDeployers()
|
||||
const availableDeployers = deployersResult.deployers || []
|
||||
|
||||
if (availableDeployers.length === 0) {
|
||||
throw new Error(
|
||||
'No deployers available. Please configure at least one deployer in the backend.'
|
||||
)
|
||||
}
|
||||
|
||||
// Use the first available deployer if none specified
|
||||
const deployerToUse =
|
||||
config.deployerLrn || availableDeployers[0]?.deployerLrn
|
||||
|
||||
if (!deployerToUse) {
|
||||
throw new Error('No valid deployer found')
|
||||
}
|
||||
|
||||
console.log('🔍 Using deployer:', deployerToUse)
|
||||
|
||||
// Get the backend's wallet address for blockchain transactions
|
||||
const backendAddress = await gqlClient.getAddress()
|
||||
console.log('🔍 Backend wallet address:', backendAddress)
|
||||
console.log('🔍 Frontend wallet address:', wallet.address)
|
||||
|
||||
// Use backend's address for blockchain transactions
|
||||
const projectData = {
|
||||
name: config.projectName,
|
||||
repository: `${githubUsername}/${config.projectName}`,
|
||||
prodBranch: 'main',
|
||||
template: config.template.id,
|
||||
paymentAddress: backendAddress,
|
||||
txHash:
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000000'
|
||||
}
|
||||
|
||||
console.log(
|
||||
'📤 Final project data being sent:',
|
||||
JSON.stringify(projectData, null, 2)
|
||||
)
|
||||
console.log('📤 With deployer:', deployerToUse)
|
||||
console.log('📤 Organization slug:', config.organizationSlug)
|
||||
console.log(
|
||||
'📤 Environment variables:',
|
||||
config.environmentVariables || []
|
||||
)
|
||||
|
||||
// Log the exact GraphQL variables being sent
|
||||
const mutationVariables = {
|
||||
organizationSlug: config.organizationSlug,
|
||||
data: projectData,
|
||||
lrn: deployerToUse,
|
||||
auctionParams: undefined,
|
||||
environmentVariables: config.environmentVariables || []
|
||||
}
|
||||
|
||||
console.log(
|
||||
'🔍 EXACT GraphQL variables being sent:',
|
||||
JSON.stringify(mutationVariables, null, 2)
|
||||
)
|
||||
|
||||
const projectResult = await gqlClient.addProject(
|
||||
config.organizationSlug,
|
||||
projectData,
|
||||
deployerToUse,
|
||||
undefined, // auctionParams
|
||||
config.environmentVariables || []
|
||||
)
|
||||
|
||||
console.log('✅ Backend response received:', projectResult)
|
||||
console.log('🔍 Project ID:', projectResult.addProject?.id)
|
||||
console.log('🔍 Full project object:', projectResult.addProject)
|
||||
|
||||
if (!projectResult.addProject?.id) {
|
||||
console.error(
|
||||
'❌ No project ID in response. Full response:',
|
||||
projectResult
|
||||
)
|
||||
throw new Error(
|
||||
'Failed to set up project deployment in backend - no project ID returned'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(
|
||||
'✅ Project created successfully with ID:',
|
||||
projectResult.addProject.id
|
||||
)
|
||||
|
||||
// Create and return the result
|
||||
const result: TemplateDeploymentResult = {
|
||||
projectId: projectResult.addProject.id,
|
||||
repositoryUrl: repoResult.repositoryUrl,
|
||||
deploymentUrl: undefined, // Will be populated once deployment completes
|
||||
deploymentId: projectResult.addProject.id
|
||||
}
|
||||
|
||||
setDeploymentResult(result)
|
||||
toast.success('Template deployed successfully!')
|
||||
|
||||
return result
|
||||
} catch (backendError) {
|
||||
console.error('❌ Backend project creation failed:', backendError)
|
||||
|
||||
// Enhanced error handling
|
||||
let errorMessage = 'Unknown deployment error'
|
||||
let errorDetails = {
|
||||
message: 'Unknown error',
|
||||
stack: undefined as string | undefined,
|
||||
name: 'UnknownError'
|
||||
}
|
||||
|
||||
if (backendError instanceof Error) {
|
||||
errorMessage = backendError.message
|
||||
errorDetails = {
|
||||
message: backendError.message,
|
||||
stack: backendError.stack,
|
||||
name: backendError.name
|
||||
}
|
||||
} else if (typeof backendError === 'object' && backendError !== null) {
|
||||
const errorObj = backendError as any
|
||||
errorMessage = errorObj.message || JSON.stringify(backendError)
|
||||
errorDetails = {
|
||||
message: errorObj.message || errorMessage,
|
||||
stack: errorObj.stack,
|
||||
name: errorObj.name || 'BackendError'
|
||||
}
|
||||
}
|
||||
|
||||
console.error('❌ Error details:', errorDetails)
|
||||
|
||||
// Provide more specific error messages
|
||||
if (
|
||||
errorMessage.includes('Cannot return null for non-nullable field')
|
||||
) {
|
||||
errorMessage = `Backend validation error: A required field is missing or invalid.
|
||||
|
||||
Possible issues:
|
||||
1. Invalid template ID: "${config.template.id}"
|
||||
2. Repository format issue: "${repoResult.repositoryUrl}"
|
||||
3. Missing deployer configuration
|
||||
4. Organization "${config.organizationSlug}" not found
|
||||
|
||||
Repository was created successfully: ${repoResult.repositoryUrl}
|
||||
You may need to check the backend configuration or contact support.`
|
||||
}
|
||||
|
||||
throw new Error(`Backend deployment setup failed: ${errorMessage}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Template deployment failed:', error)
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
setError(errorMessage)
|
||||
toast.error(`Template deployment failed: ${errorMessage}`)
|
||||
throw error
|
||||
} finally {
|
||||
setIsDeploying(false)
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
setDeploymentResult(null)
|
||||
setError(null)
|
||||
setIsDeploying(false)
|
||||
}
|
||||
|
||||
return {
|
||||
deployTemplate,
|
||||
isDeploying: isDeploying || directGitHub.isLoading,
|
||||
deploymentResult,
|
||||
error,
|
||||
reset
|
||||
}
|
||||
}
|
69
apps/deploy-fe/src/hooks/useGitHubToken.tsx
Normal file
69
apps/deploy-fe/src/hooks/useGitHubToken.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
|
||||
import { getGitHubToken } from '@/actions/github'
|
||||
import { useAuth } from '@clerk/nextjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface UseGitHubTokenReturn {
|
||||
token: string | null
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
refreshToken: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to get the current user's GitHub OAuth token from Clerk
|
||||
* This ensures each user gets their own token for GitHub API calls
|
||||
*/
|
||||
export function useGitHubToken(): UseGitHubTokenReturn {
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const { isLoaded: isAuthLoaded, userId } = useAuth()
|
||||
|
||||
const fetchToken = async () => {
|
||||
if (!userId) {
|
||||
setError('User not authenticated')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
const userToken = await getGitHubToken()
|
||||
|
||||
if (!userToken) {
|
||||
setError(
|
||||
'GitHub account not connected. Please connect your GitHub account.'
|
||||
)
|
||||
setToken(null)
|
||||
} else {
|
||||
console.log('✅ Successfully retrieved user-specific GitHub token')
|
||||
setToken(userToken)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Error getting GitHub token:', err)
|
||||
setError(
|
||||
err instanceof Error ? err.message : 'Failed to get GitHub token'
|
||||
)
|
||||
setToken(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthLoaded) {
|
||||
fetchToken()
|
||||
}
|
||||
}, [isAuthLoaded, userId])
|
||||
|
||||
const refreshToken = async () => {
|
||||
await fetchToken()
|
||||
}
|
||||
|
||||
return { token, isLoading, error, refreshToken }
|
||||
}
|
@ -1,14 +1,13 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useAuth, useUser } from "@clerk/nextjs";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { useOctokit } from '@/context/OctokitContext'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// Define the return type of the hook
|
||||
interface UseRepoDataReturn {
|
||||
repoData: any;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
repoData: any
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
@ -18,121 +17,111 @@ interface UseRepoDataReturn {
|
||||
* @returns Object containing repository data, loading state, and any errors
|
||||
*/
|
||||
export function useRepoData(repoId: string): UseRepoDataReturn {
|
||||
const [repoData, setRepoData] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [octokit, setOctokit] = useState<Octokit | null>(null);
|
||||
const [repoData, setRepoData] = useState<any>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Get auth data from Clerk
|
||||
const { isLoaded: isAuthLoaded } = useAuth();
|
||||
const { isLoaded: isUserLoaded, user } = useUser();
|
||||
// Use the centralized Octokit context instead of creating our own
|
||||
const { octokit, isAuth } = useOctokit()
|
||||
|
||||
// Initialize Octokit with the appropriate token
|
||||
// Debug the context state
|
||||
useEffect(() => {
|
||||
async function initializeOctokit() {
|
||||
let token = null;
|
||||
console.log('🔍 useRepoData: Context state changed:', {
|
||||
hasOctokit: !!octokit,
|
||||
isAuth,
|
||||
repoId
|
||||
})
|
||||
}, [octokit, isAuth, repoId])
|
||||
|
||||
// Try to get GitHub token from Clerk
|
||||
if (user) {
|
||||
try {
|
||||
// Check if user has connected GitHub account
|
||||
const githubAccount = user.externalAccounts.find(
|
||||
account => account.provider === 'github'
|
||||
);
|
||||
|
||||
if (githubAccount) {
|
||||
// Try to get GitHub OAuth token from Clerk
|
||||
try {
|
||||
// token = await user.getToken({ template: 'github' });
|
||||
console.log('Using GitHub token from Clerk');
|
||||
} catch (err) {
|
||||
console.error('Error getting GitHub token from Clerk:', err);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error accessing Clerk user data:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to token from environment variable
|
||||
if (!token && typeof process !== 'undefined') {
|
||||
token = process.env.NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN || '';
|
||||
if (token) {
|
||||
console.warn('Using fallback GitHub token. This should only be used for development.');
|
||||
}
|
||||
}
|
||||
|
||||
// Create Octokit instance with whatever token we found
|
||||
if (token) {
|
||||
setOctokit(new Octokit({ auth: token }));
|
||||
} else {
|
||||
setError("No GitHub token available");
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAuthLoaded && isUserLoaded) {
|
||||
initializeOctokit();
|
||||
}
|
||||
}, [isAuthLoaded, isUserLoaded, user]);
|
||||
|
||||
// Fetch repo data when Octokit is available
|
||||
// Fetch repo data when Octokit is available and authenticated
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
let isMounted = true
|
||||
|
||||
async function fetchRepoData() {
|
||||
if (!octokit) {
|
||||
return;
|
||||
// Don't attempt to fetch if not authenticated
|
||||
if (!isAuth) {
|
||||
console.log('🔍 fetchRepoData: Not authenticated, skipping fetch')
|
||||
if (isMounted) {
|
||||
setError('GitHub authentication required')
|
||||
setRepoData(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!octokit) {
|
||||
console.log('🔍 fetchRepoData: No octokit instance available')
|
||||
if (isMounted) {
|
||||
setError('GitHub client not available')
|
||||
setRepoData(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🔍 fetchRepoData: Starting to fetch repos...')
|
||||
console.log('🔍 fetchRepoData: isAuth =', isAuth)
|
||||
|
||||
try {
|
||||
// Fetch repos from GitHub
|
||||
const { data: repos } = await octokit.repos.listForAuthenticatedUser();
|
||||
console.log('🔍 Making GitHub API call: octokit.rest.repos.listForAuthenticatedUser()')
|
||||
const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser()
|
||||
console.log('🔍 GitHub API success! Received', repos.length, 'repositories')
|
||||
|
||||
// If no repoId is provided, return all repos
|
||||
if (!repoId) {
|
||||
if (isMounted) {
|
||||
setRepoData(repos);
|
||||
setError(null);
|
||||
setIsLoading(false);
|
||||
setRepoData(repos)
|
||||
setError(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Find the specific repo by ID if repoId is provided
|
||||
const repo = repos.find(repo => repo.id.toString() === repoId);
|
||||
const repo = repos.find((repo: any) => repo.id.toString() === repoId)
|
||||
|
||||
if (!repo) {
|
||||
if (isMounted) {
|
||||
setError("Repository not found");
|
||||
setRepoData(null);
|
||||
setIsLoading(false);
|
||||
setError('Repository not found')
|
||||
setRepoData(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
} else {
|
||||
if (isMounted) {
|
||||
setRepoData(repo);
|
||||
setError(null);
|
||||
setIsLoading(false);
|
||||
setRepoData(repo)
|
||||
setError(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching GitHub repo:', err);
|
||||
console.error('❌ Error fetching GitHub repo:', err)
|
||||
console.error('❌ Error details:', {
|
||||
message: err instanceof Error ? err.message : 'Unknown error',
|
||||
status: (err as any)?.status,
|
||||
response: (err as any)?.response?.data,
|
||||
})
|
||||
if (isMounted) {
|
||||
setError('Failed to fetch repository data');
|
||||
setRepoData(null);
|
||||
setIsLoading(false);
|
||||
setError('Failed to fetch repository data')
|
||||
setRepoData(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (octokit) {
|
||||
fetchRepoData();
|
||||
if (octokit && isAuth) {
|
||||
fetchRepoData()
|
||||
} else if (!isAuth) {
|
||||
// Handle case where we're not authenticated yet
|
||||
console.log('🔍 useRepoData: Waiting for authentication...')
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [repoId, octokit]);
|
||||
isMounted = false
|
||||
}
|
||||
}, [repoId, octokit, isAuth])
|
||||
|
||||
return { repoData, isLoading, error };
|
||||
return { repoData, isLoading, error }
|
||||
}
|
3
apps/deployer/.env.example
Normal file
3
apps/deployer/.env.example
Normal file
@ -0,0 +1,3 @@
|
||||
REGISTRY_BOND_ID=
|
||||
DEPLOYER_LRN=
|
||||
AUTHORITY=
|
@ -1,5 +1,9 @@
|
||||
# deployer
|
||||
|
||||
The following steps are for deploying the frontend app to vaasl webapp deployer.
|
||||
|
||||
Run these steps in the `apps/deployer/` directory:
|
||||
|
||||
- Install dependencies
|
||||
|
||||
```bash
|
||||
@ -7,46 +11,60 @@
|
||||
```
|
||||
|
||||
```bash
|
||||
brew install jq # if you do not have jq installed already
|
||||
# if you do not have jq installed already
|
||||
brew install jq
|
||||
# OR
|
||||
sudo apt-get install jq
|
||||
```
|
||||
|
||||
- Run script to deploy app
|
||||
- Copy and update `.env`
|
||||
|
||||
- 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
|
||||
```
|
||||
|
||||
```bash
|
||||
./deploy-frontend.sh
|
||||
```
|
||||
- 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_BACKEND_CLIENT_ID: Ov23li1hxnCcEggrbwED
|
||||
```
|
||||
|
||||
NOTE: Replace `your_clerk_key` and `your_clerk_secret` with actual values (<https://clerk.com/docs/quickstarts/nextjs-pages-router#set-your-clerk-api-keys>)
|
||||
|
||||
- Run script to deploy app to `deploy-staging.laconic.co`
|
||||
|
||||
```bash
|
||||
./deploy-frontend.sh
|
||||
```
|
||||
|
||||
- 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
|
||||
|
||||
- Check deployment status in [web-app deployer](https://console.laconic.com/deployer).
|
||||
|
||||
- Check records in [registry console app](https://console.laconic.com/#/registry).
|
||||
|
||||
- If deployment fails due to low bond balance
|
||||
|
||||
- Check balances
|
||||
|
||||
```bash
|
||||
@ -54,11 +72,11 @@
|
||||
pnpm laconic registry account get
|
||||
|
||||
# Bond balance
|
||||
pnpm laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
pnpm laconic registry bond get --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38
|
||||
```
|
||||
|
||||
- Command to refill bond
|
||||
|
||||
```bash
|
||||
pnpm laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000
|
||||
pnpm laconic registry bond refill --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38 --type alnt --quantity 10000000
|
||||
```
|
||||
|
@ -1,8 +1,8 @@
|
||||
services:
|
||||
registry:
|
||||
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||
rpcEndpoint: https://laconicd-mainnet-1.laconic.com/
|
||||
gqlEndpoint: https://laconicd-mainnet-1.laconic.com/api
|
||||
userKey:
|
||||
bondId:
|
||||
chainId: laconic-testnet-2
|
||||
bondId: 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38
|
||||
chainId: laconic-mainnet
|
||||
gasPrice: 0.001alnt
|
||||
|
@ -6,7 +6,7 @@ echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
echo "Using AUTHORITY: $AUTHORITY"
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
REPO_URL="https://git.vdb.to/NasSharaf/laconic-deployer-frontend"
|
||||
|
||||
# Get the latest commit hash for a branch
|
||||
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
|
||||
|
||||
# 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 "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}')
|
||||
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}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
@ -39,8 +39,8 @@ record:
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: deploy-frontend
|
||||
app_type: webapp/next
|
||||
name: laconic-deployer-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
@ -60,7 +60,7 @@ echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/laconic-deployer-frontend"
|
||||
|
||||
sleep 2
|
||||
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
@ -117,24 +117,24 @@ else
|
||||
echo "Payment amount is null; skipping payment."
|
||||
fi
|
||||
|
||||
# TODO: Update dns when transitioning from staging to production deployment (deploy.laconic.com)
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: deploy-frontend@$PACKAGE_VERSION
|
||||
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
||||
name: laconic-deployer-frontend@$PACKAGE_VERSION
|
||||
application: lrn://$AUTHORITY/applications/laconic-deployer-frontend@$PACKAGE_VERSION
|
||||
deployer: $DEPLOYER_LRN
|
||||
dns: deploy.laconic.com
|
||||
dns: deploy-staging.laconic.com
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.laconic.com
|
||||
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
|
||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||
LACONIC_HOSTED_CONFIG_wallet_iframe_url: https://wallet.laconic.com
|
||||
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_CLERK_PUBLISHABLE_KEY:
|
||||
CLERK_SECRET_KEY:
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID: Ov23li1hxnCcEggrbwED
|
||||
meta:
|
||||
note: Added @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
@ -142,6 +142,12 @@ record:
|
||||
payment: $txHash
|
||||
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
|
||||
|
||||
sleep 2
|
||||
|
16
build-webapp.sh
Executable file
16
build-webapp.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/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_BACKEND_CLIENT_ID=CERC_RUNTIME_ENV_NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID
|
||||
EOF
|
||||
|
||||
pnpm install || exit 1
|
||||
|
||||
pnpm build:fe-compile || exit 1
|
@ -5,6 +5,7 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "turbo build",
|
||||
"build:fe-compile": "turbo build:compile --filter=deploy-fe",
|
||||
"dev": "turbo dev",
|
||||
"lint": "turbo lint",
|
||||
"start": "turbo start",
|
||||
|
2073
pnpm-lock.yaml
generated
2073
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
268
readme.md
268
readme.md
@ -6,13 +6,13 @@ Ensure you have the following installed:
|
||||
- [Node.js](https://nodejs.org/) (v18+)
|
||||
- [pnpm](https://pnpm.io/) (v8+)
|
||||
- [Git](https://git-scm.com/)
|
||||
- [ngrok](https://ngrok.com/docs/getting-started/#2-install-the-ngrok-agent-cli)
|
||||
|
||||
## Project Structure
|
||||
|
||||
This monorepo contains several packages:
|
||||
|
||||
- `apps/deploy-fe`: Frontend Next.js application
|
||||
- `apps/backend`: Express.js backend API
|
||||
- `apps/deployer`: Deployment service
|
||||
- `services/gql-client`: GraphQL client library
|
||||
- `services/ui`: Shared UI components
|
||||
@ -20,48 +20,232 @@ This monorepo contains several packages:
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Clone the repository
|
||||
The following steps are for running the deploy app locally (using [laconicd mainnet chain](https://laconicd-mainnet-1.laconic.com/status)):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/qwrk-laconic-core-develop.git
|
||||
cd qwrk-laconic-core-develop
|
||||
```
|
||||
- Clone laconic-wallet-web repo
|
||||
```bash
|
||||
git clone https://git.vdb.to/LaconicNetwork/laconic-wallet-web.git
|
||||
cd laconic-wallet-web
|
||||
```
|
||||
|
||||
### 2. Install dependencies
|
||||
- Create .env
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
- Update values in `.env`
|
||||
```
|
||||
# Not required since WalletConnect is not used in iframe integration
|
||||
REACT_APP_WALLET_CONNECT_PROJECT_ID=
|
||||
|
||||
### 3. Configure environment variables
|
||||
REACT_APP_DEFAULT_GAS_PRICE=0.025
|
||||
|
||||
Create a `.env.local` file in the `apps/deploy-fe` directory:
|
||||
# Reference: https://github.com/cosmos/cosmos-sdk/issues/16020
|
||||
REACT_APP_GAS_ADJUSTMENT=2
|
||||
REACT_APP_LACONICD_RPC_URL=https://laconicd-mainnet-1.laconic.com
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_key
|
||||
CLERK_SECRET_KEY=your_clerk_secret
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID=laconic-testnet-2
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=your_github_token
|
||||
```
|
||||
# URL of Deploy app frontend app that will run locally
|
||||
REACT_APP_ALLOWED_URLS=http://localhost:3000
|
||||
```
|
||||
|
||||
Create a `local.toml` file in `apps/backend/environments/` (based on the example file).
|
||||
- Install dependencies
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 4. Start the development servers
|
||||
- Build app
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
#### Frontend:
|
||||
```bash
|
||||
cd apps/deploy-fe
|
||||
pnpm dev
|
||||
```
|
||||
- Set env values in build
|
||||
```bash
|
||||
yarn set-env
|
||||
```
|
||||
|
||||
### 5. Connect Your Wallet
|
||||
- Serve the build
|
||||
```
|
||||
# Install package serve globally
|
||||
npm install -g serve
|
||||
|
||||
The application requires a Laconic wallet for certain operations. You need to set up:
|
||||
# Serve the wallet build
|
||||
serve -s -l 4000 ./build
|
||||
```
|
||||
|
||||
Wallet will run at http://localhost:4000
|
||||
|
||||
- In a new terminal, clone repo for backend
|
||||
```bash
|
||||
git clone https://git.vdb.to/cerc-io/snowballtools-base.git
|
||||
cd snowballtools-base
|
||||
```
|
||||
|
||||
- Install deps and build
|
||||
```bash
|
||||
yarn && yarn build --ignore frontend
|
||||
```
|
||||
|
||||
- Create `packages/backend/environments/local.toml`
|
||||
```bash
|
||||
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
|
||||
```
|
||||
|
||||
- Update values in `packages/backend/environments/local.toml`
|
||||
```toml
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 8000
|
||||
gqlPath = "/graphql"
|
||||
[server.session]
|
||||
# Can be set to any random string
|
||||
secret = "RpwqcvFkLZ"
|
||||
# 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 = "Ov23lihCWQvOORNxtebD"
|
||||
clientSecret = "139de35bdd610628289adcd99bb2c7c962a11c99"
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
checkAuctionStatusDelay = 5000
|
||||
restEndpoint = "https://laconicd-mainnet-1.laconic.com"
|
||||
gqlEndpoint = "https://laconicd-mainnet-1.laconic.com/api"
|
||||
chainId = "laconic-mainnet"
|
||||
# Set private key of account laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef
|
||||
# This account owns the bond and authority configured below
|
||||
privateKey = ""
|
||||
bondId = "230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38"
|
||||
authority = "laconic"
|
||||
[registryConfig.fee]
|
||||
gasPrice = "0.001alnt"
|
||||
|
||||
[auction]
|
||||
commitFee = "1000"
|
||||
commitsDuration = "60s"
|
||||
revealFee = "1000"
|
||||
revealsDuration = "60s"
|
||||
denom = "alnt"
|
||||
```
|
||||
|
||||
- Run ngrok
|
||||
```bash
|
||||
ngrok http 8000
|
||||
```
|
||||
|
||||
- Set ngrok URL to `gitHub.webhookUrl` in config `packages/backend/environments/local.toml`
|
||||
- Example
|
||||
```toml
|
||||
...
|
||||
[gitHub]
|
||||
webhookUrl = "https://<ngrok-url>.ngrok-free.app"
|
||||
...
|
||||
```
|
||||
|
||||
- Run backend server
|
||||
```bash
|
||||
cd packages/backend
|
||||
yarn start
|
||||
```
|
||||
|
||||
- In a new terminal, clone repo for frontend (laconic-deployer-frontend)
|
||||
```bash
|
||||
git clone https://git.vdb.to/NasSharaf/laconic-deployer-frontend.git
|
||||
cd laconic-deployer-frontend
|
||||
```
|
||||
|
||||
- Install dependencies
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- Build dependecies for frontend app
|
||||
```bash
|
||||
pnpm build:fe-compile
|
||||
```
|
||||
|
||||
- Create a `.env.local` file in the `apps/deploy-fe` directory to configure environment variables:
|
||||
```
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_key
|
||||
CLERK_SECRET_KEY=your_clerk_secret
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID=laconic-mainnet
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID=Ov23lihCWQvOORNxtebD
|
||||
```
|
||||
Replace `your_clerk_key` and `your_clerk_secret` with actual values (<https://clerk.com/docs/quickstarts/nextjs-pages-router#set-your-clerk-api-keys>)
|
||||
|
||||
- Start the development server
|
||||
```bash
|
||||
cd apps/deploy-fe
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- The deploy frontend app will be now available at <http://localhost:3000>
|
||||
|
||||
### Fund wallet account
|
||||
|
||||
- Change directory to deployer package
|
||||
```bash
|
||||
cd apps/deployer/
|
||||
```
|
||||
|
||||
- Setup config for laconic-registry-cli in `packages/deployer/config.yml`
|
||||
```yaml
|
||||
services:
|
||||
registry:
|
||||
rpcEndpoint: https://laconicd-mainnet-1.laconic.com
|
||||
gqlEndpoint: https://laconicd-mainnet-1.laconic.com/api
|
||||
# Set to private key of an account with funds
|
||||
# Private key of account laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef set in deployer backend can be used
|
||||
userKey:
|
||||
chainId: laconic-testnet-2
|
||||
gasPrice: 0.001alnt
|
||||
```
|
||||
|
||||
- Get wallet account address at <http://localhost:4000> for `laconicd mainnet` network
|
||||
|
||||
- Run command to send tokens
|
||||
```bash
|
||||
pnpm laconic registry tokens send --address <ACCOUNT_ADDRESS_FROM_WALLET> --type alnt --quantity 12960
|
||||
```
|
||||
|
||||
- To check balance visit <https://explorer.laconic.com/laconic-mainnet/account/ACCOUNT_ADDRESS_FROM_WALLET>
|
||||
|
||||
### Misc
|
||||
|
||||
- To check for deployments in vaasl : <https://webapp-deployer-ui.apps.vaasl.io/>
|
||||
|
||||
- If deployment fails due to low bond balance
|
||||
- Change directory to deployer package
|
||||
```bash
|
||||
cd apps/deployer/
|
||||
```
|
||||
|
||||
- Check balances
|
||||
```bash
|
||||
# Account balance
|
||||
yarn laconic registry account get --address laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef
|
||||
|
||||
# Bond balance
|
||||
yarn laconic registry bond get --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38
|
||||
```
|
||||
|
||||
- Command to refill bond
|
||||
|
||||
```bash
|
||||
yarn laconic registry bond refill --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38 --type alnt --quantity 10000000
|
||||
```
|
||||
|
||||
1. A running wallet instance (follow the setup in the laconic-wallet-web repository)
|
||||
2. Configure the `NEXT_PUBLIC_WALLET_IFRAME_URL` to point to your wallet instance
|
||||
|
||||
## Architecture
|
||||
|
||||
@ -93,3 +277,25 @@ Make sure you have:
|
||||
1. Connected your GitHub account in Clerk
|
||||
2. Provided the necessary permissions for repository access
|
||||
3. Set a fallback GitHub token in `.env.local` if needed for development
|
||||
|
||||
## Test deployment
|
||||
|
||||
- Install stack-orchestrator from <https://git.vdb.to/cerc-io/stack-orchestrator#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)
|
||||
|
17
run-webapp.sh
Executable file
17
run-webapp.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/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,6 +8,11 @@
|
||||
"outputs": [".next/**", "!.next/cache/**"],
|
||||
"env": ["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY"]
|
||||
},
|
||||
"build:compile": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"start": {
|
||||
"dependsOn": ["^build"],
|
||||
"cache": false,
|
||||
|
Loading…
Reference in New Issue
Block a user