Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a34373d9be | ||
|
|
99be851046 | ||
|
|
2b408f8a63 | ||
|
|
33cd907455 | ||
|
|
022ffbe589 | ||
|
|
7bb5f9831c | ||
|
|
aabda9b486 |
@@ -19,7 +19,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- name: Build libs
|
|
||||||
run: yarn workspace gql-client run build
|
|
||||||
- name: Linter check
|
- name: Linter check
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
name: Test webapp deployment
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 3 * * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test_app_deployment:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [20.x]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn
|
|
||||||
- name: Test webapp deployment
|
|
||||||
run: ./packages/deployer/test/test-webapp-deployment-undeployment.sh
|
|
||||||
- name: Notify Vulcanize Slack on CI failure
|
|
||||||
if: ${{ always() && github.ref_name == 'main' }}
|
|
||||||
uses: ravsamhq/notify-slack-action@v2
|
|
||||||
with:
|
|
||||||
status: ${{ job.status }}
|
|
||||||
notify_when: 'failure'
|
|
||||||
env:
|
|
||||||
SLACK_WEBHOOK_URL: ${{ secrets.VULCANIZE_SLACK_CI_ALERTS_WEBHOOK }}
|
|
||||||
- name: Notify DeepStack Slack on CI failure
|
|
||||||
if: ${{ always() && github.ref_name == 'main' }}
|
|
||||||
uses: ravsamhq/notify-slack-action@v2
|
|
||||||
with:
|
|
||||||
status: ${{ job.status }}
|
|
||||||
notify_when: 'failure'
|
|
||||||
env:
|
|
||||||
SLACK_WEBHOOK_URL: ${{ secrets.DEEPSTACK_SLACK_CI_ALERTS_WEBHOOK }}
|
|
||||||
@@ -1,9 +1 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
yarn-error.log
|
|
||||||
.yarnrc.yml
|
|
||||||
.yarn/
|
|
||||||
.yarnrc
|
|
||||||
|
|
||||||
packages/backend/environments/local.toml
|
|
||||||
packages/backend/dev/
|
|
||||||
packages/frontend/dist/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
v20.12.1
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
// IntelliSense for taiwind variants
|
// IntelliSense for taiwind variants
|
||||||
"tailwindCSS.experimental.classRegex": [
|
"tailwindCSS.experimental.classRegex": [
|
||||||
"tv\\('([^)]*)\\')",
|
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||||
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
# snowballtools-base
|
# snowballtools
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
- Clone the `snowballtools-base` repo
|
- Clone the `snowballtools` repo
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone git@github.com:snowball-tools/snowballtools-base.git
|
git clone git@github.com:snowball-tools/snowballtools-base.git
|
||||||
cd snowballtools-base
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Install dependencies in root
|
- In root of the repo, install depedencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn
|
yarn
|
||||||
@@ -21,92 +20,50 @@
|
|||||||
yarn build --ignore frontend
|
yarn build --ignore frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
Let us assume the following domains for backend and frontend
|
## Backend
|
||||||
- Backend server: `api.snowballtools.com`
|
|
||||||
- Frontend app: `dashboard.snowballtools.com`
|
|
||||||
|
|
||||||
- Configuration Files
|
- Change directory to `packages/backend`
|
||||||
|
|
||||||
- For Backend:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
|
cd packages/backend
|
||||||
```
|
```
|
||||||
|
|
||||||
- Production
|
- Load fixtures in database
|
||||||
|
|
||||||
- Update the following in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[server]
|
|
||||||
...
|
|
||||||
[server.session]
|
|
||||||
# Secret should be changed to a different random string
|
|
||||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
|
|
||||||
# Set URL of the frontend app
|
|
||||||
appOriginUrl = "https://dashboard.snowballtools.com"
|
|
||||||
# Set to true for session cookies to work behind proxy
|
|
||||||
trustProxy = true
|
|
||||||
# Set empty domain when using secure connection
|
|
||||||
domain = ""
|
|
||||||
```
|
|
||||||
|
|
||||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
|
|
||||||
- Homepage URL: `https://dashboard.snowballtools.com`
|
|
||||||
- Authorization callback URL: `https://dashboard.snowballtools.com/organization/projects/create`
|
|
||||||
- Generate a new client secret after app is created
|
|
||||||
|
|
||||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[gitHub]
|
|
||||||
webhookUrl = "https://api.snowballtools.com"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[registryConfig]
|
|
||||||
fetchDeploymentRecordDelay = 5000
|
|
||||||
# Use actual port for REST endpoint
|
|
||||||
restEndpoint = "http://api.laconic.com:1317"
|
|
||||||
# Use actual port for GQL endpoint
|
|
||||||
gqlEndpoint = "http://api.laconic.com:9473/api"
|
|
||||||
# Set private key of account to be used in Laconicd
|
|
||||||
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
|
|
||||||
# Set Bond ID to be used for publishing records
|
|
||||||
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
|
|
||||||
chainId = "laconic_9000-1"
|
|
||||||
# Set authority that is existing in the chain
|
|
||||||
authority = "laconic"
|
|
||||||
[registryConfig.fee]
|
|
||||||
amount = "200000"
|
|
||||||
denom = "aphoton"
|
|
||||||
gas = "750000"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Development
|
|
||||||
|
|
||||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
|
|
||||||
- Homepage URL: `http://localhost:3000`
|
|
||||||
- Authorization callback URL: `http://localhost:3000/organization/projects/create`
|
|
||||||
- Generate a new client secret after app is created
|
|
||||||
|
|
||||||
- Setup Laconicd
|
|
||||||
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
|
|
||||||
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
laconic-so deployment --dir laconic-loaded-deployment exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
|
yarn test:db:load:fixtures
|
||||||
|
```
|
||||||
|
|
||||||
|
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
- Client ID and secret will be available after creating Github OAuth app
|
||||||
|
- https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app
|
||||||
|
- In "Homepage URL", type `http://localhost:3000`
|
||||||
|
- In "Authorization callback URL", type `http://localhost:3000/organization/projects/create`
|
||||||
|
- Generate a new client secret after app is created
|
||||||
|
|
||||||
|
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
|
||||||
|
|
||||||
|
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
laconic-so --stack fixturenet-laconic-loaded deploy exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
|
||||||
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
|
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
|
||||||
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
|
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
|
||||||
```
|
```
|
||||||
|
|
||||||
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
|
- Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
|
||||||
- Run the script to create bond, reserve the authority and set authority bond
|
```bash
|
||||||
|
# For registryConfig.restEndpoint
|
||||||
|
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
|
||||||
|
# 0.0.0.0:32777
|
||||||
|
|
||||||
|
# For registryConfig.gqlEndpoint
|
||||||
|
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
|
||||||
|
# 0.0.0.0:32771
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run the script to create bond, reserve the authority and set authority bond
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn test:registry:init
|
yarn test:registry:init
|
||||||
@@ -115,106 +72,79 @@ Let us assume the following domains for backend and frontend
|
|||||||
|
|
||||||
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
|
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
|
||||||
- Setup ngrok for GitHub webhooks
|
- Setup ngrok for GitHub webhooks
|
||||||
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
|
- https://ngrok.com/docs/getting-started/
|
||||||
- Start ngrok and point to backend server endpoint
|
- Start ngrok and point to backend server endpoint
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ngrok http http://localhost:8000
|
ngrok http http://localhost:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
- Look for the forwarding URL in ngrok
|
- Look for the forwarding URL in ngrok
|
||||||
|
|
||||||
```bash
|
|
||||||
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
|
|
||||||
```
|
```
|
||||||
|
...
|
||||||
|
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
|
||||||
|
...
|
||||||
|
```
|
||||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
...
|
||||||
[gitHub]
|
[gitHub]
|
||||||
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
|
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
- For Frontend:
|
- Start the server in `packages/backend`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp packages/frontend/.env.example packages/frontend/.env
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
## Frontend
|
||||||
|
|
||||||
|
- Change directory to `packages/frontend` in a new terminal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
|
||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_GITHUB_CLIENT_ID = <CLIENT_ID>
|
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
|
||||||
```
|
```
|
||||||
|
|
||||||
- Set `VITE_GITHUB_PWA_TEMPLATE_REPO` and `VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_GITHUB_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo
|
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
|
||||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Production
|
- Set `REACT_APP_GITHUB_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
- Set the following values in [.env](packages/frontend/.env) file
|
|
||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint
|
REACT_APP_GITHUB_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
||||||
```
|
```
|
||||||
|
|
||||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
|
### Development
|
||||||
- Create a project and add information to use wallet connect SDK
|
|
||||||
- Add project name and select project type as `App`
|
|
||||||
- Set project home page URL to `https://dashboard.snowballtools.com`
|
|
||||||
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
|
||||||
|
|
||||||
```env
|
- Start the React application
|
||||||
VITE_WALLET_CONNECT_ID = <PROJECT_ID>
|
|
||||||
|
```bash
|
||||||
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
- Build the React application
|
- The React application will be running in `http://localhost:3000/`
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
- Build the React application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn build
|
yarn build
|
||||||
```
|
```
|
||||||
|
|
||||||
- Use a web server for hosting static built files
|
- Use a web server for hosting static built files
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m http.server -d build 3000
|
python3 -m http.server -d build 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
- Development
|
|
||||||
|
|
||||||
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_SERVER_URL = 'http://localhost:8000'
|
|
||||||
```
|
|
||||||
|
|
||||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
|
|
||||||
- Create a project and add information to use wallet connect SDK
|
|
||||||
- Add project name and select project type as `App`
|
|
||||||
- Project home page URL is not required to be set
|
|
||||||
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_WALLET_CONNECT_ID = <Project_ID>
|
|
||||||
```
|
|
||||||
|
|
||||||
- The React application will be running in `http://localhost:3000/`
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
- Start the backend server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn start:backend
|
|
||||||
```
|
|
||||||
|
|
||||||
- Start the frontend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
PKG_DIR="./packages/frontend"
|
PKG_DIR="./packages/frontend"
|
||||||
OUTPUT_DIR="${PKG_DIR}/dist"
|
OUTPUT_DIR="${PKG_DIR}/build"
|
||||||
DEST_DIR=${1:-/data}
|
DEST_DIR=${1:-/data}
|
||||||
|
|
||||||
if [[ -d "$DEST_DIR" ]]; then
|
if [[ -d "$DEST_DIR" ]]; then
|
||||||
@@ -10,17 +10,9 @@ if [[ -d "$DEST_DIR" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
cat > $PKG_DIR/.env <<EOF
|
cat > $PKG_DIR/.env <<EOF
|
||||||
VITE_SERVER_URL = 'LACONIC_HOSTED_CONFIG_server_url'
|
REACT_APP_GQL_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_gql_url'
|
||||||
VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid'
|
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
|
||||||
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
|
REACT_APP_GITHUB_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_templaterepo'
|
||||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
|
|
||||||
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
|
|
||||||
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
|
|
||||||
VITE_ALCHEMY_API_KEY = 'LACONIC_HOSTED_CONFIG_aplchemy_api_key'
|
|
||||||
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
|
|
||||||
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'
|
|
||||||
VITE_TURNKEY_API_BASE_URL = 'LACONIC_HOSTED_CONFIG_turnkey_api_base_url'
|
|
||||||
VITE_TURNKEY_ORGANIZATION_ID = 'LACONIC_HOSTED_CONFIG_turnkey_organization_id'
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
yarn || exit 1
|
yarn || exit 1
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"depcheck": "^1.4.2",
|
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lerna": "^8.0.0",
|
"lerna": "^8.0.0",
|
||||||
"patch-package": "^8.0.0"
|
"depcheck": "^1.4.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"build": "lerna run build --stream",
|
"build": "lerna run build --stream",
|
||||||
"lint": "lerna run lint --stream"
|
"lint": "lerna run lint --stream -- --max-warnings=0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
db
|
db
|
||||||
dist
|
dist
|
||||||
environments/local.toml
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
v20.12.1
|
|
||||||
@@ -2,11 +2,6 @@
|
|||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = 8000
|
port = 8000
|
||||||
gqlPath = "/graphql"
|
gqlPath = "/graphql"
|
||||||
[server.session]
|
|
||||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
|
|
||||||
appOriginUrl = "http://localhost:3000"
|
|
||||||
trustProxy = false
|
|
||||||
domain = "localhost"
|
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
dbPath = "db/snowball"
|
dbPath = "db/snowball"
|
||||||
@@ -24,11 +19,7 @@
|
|||||||
chainId = "laconic_9000-1"
|
chainId = "laconic_9000-1"
|
||||||
privateKey = ""
|
privateKey = ""
|
||||||
bondId = ""
|
bondId = ""
|
||||||
authority = ""
|
|
||||||
[registryConfig.fee]
|
[registryConfig.fee]
|
||||||
amount = "200000"
|
amount = "200000"
|
||||||
denom = "aphoton"
|
denom = "aphoton"
|
||||||
gas = "750000"
|
gas = "550000"
|
||||||
|
|
||||||
[misc]
|
|
||||||
projectDomain = "apps.snowballtools.com"
|
|
||||||
@@ -1,25 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"license": "UNLICENSED",
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@cerc-io/laconic-sdk": "^0.1.14",
|
||||||
"@graphql-tools/schema": "^10.0.2",
|
"@graphql-tools/schema": "^10.0.2",
|
||||||
"@graphql-tools/utils": "^10.0.12",
|
"@graphql-tools/utils": "^10.0.12",
|
||||||
"@octokit/oauth-app": "^6.1.0",
|
"@octokit/oauth-app": "^6.1.0",
|
||||||
"@snowballtools/laconic-sdk": "^0.1.17",
|
|
||||||
"@turnkey/sdk-server": "^0.1.0",
|
|
||||||
"@types/debug": "^4.1.5",
|
"@types/debug": "^4.1.5",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
"@types/semver": "^7.5.8",
|
|
||||||
"apollo-server-core": "^3.13.0",
|
"apollo-server-core": "^3.13.0",
|
||||||
"apollo-server-express": "^3.13.0",
|
"apollo-server-express": "^3.13.0",
|
||||||
"cookie-session": "^2.1.0",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.18.0",
|
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
@@ -39,21 +33,29 @@
|
|||||||
"copy-assets": "copyfiles -u 1 src/**/*.gql dist/",
|
"copy-assets": "copyfiles -u 1 src/**/*.gql dist/",
|
||||||
"clean": "rm -rf ./dist",
|
"clean": "rm -rf ./dist",
|
||||||
"build": "yarn clean && tsc && yarn copy-assets",
|
"build": "yarn clean && tsc && yarn copy-assets",
|
||||||
|
"lint": "eslint .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"lint": "tsc --noEmit",
|
|
||||||
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
|
"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-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:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
|
||||||
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
|
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cookie-session": "^2.0.49",
|
|
||||||
"@types/express-session": "^1.17.10",
|
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||||
|
"@typescript-eslint/parser": "^6.18.1",
|
||||||
"better-sqlite3": "^9.2.2",
|
"better-sqlite3": "^9.2.2",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-config-semistandard": "^15.0.1",
|
||||||
|
"eslint-config-standard": "^16.0.3",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
|
"eslint-plugin-standard": "^5.0.0",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"workspace": "^0.0.1-preview.1"
|
"workspace": "^0.0.1-preview.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,7 @@
|
|||||||
export interface SessionConfig {
|
|
||||||
secret: string;
|
|
||||||
appOriginUrl: string;
|
|
||||||
trustProxy: boolean;
|
|
||||||
domain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
gqlPath?: string;
|
gqlPath?: string;
|
||||||
sessionSecret: string;
|
|
||||||
appOriginUrl: string;
|
|
||||||
isProduction: boolean;
|
|
||||||
session: SessionConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseConfig {
|
export interface DatabaseConfig {
|
||||||
@@ -34,7 +23,6 @@ export interface RegistryConfig {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
bondId: string;
|
bondId: string;
|
||||||
fetchDeploymentRecordDelay: number;
|
fetchDeploymentRecordDelay: number;
|
||||||
authority: string;
|
|
||||||
fee: {
|
fee: {
|
||||||
amount: string;
|
amount: string;
|
||||||
denom: string;
|
denom: string;
|
||||||
@@ -42,20 +30,9 @@ export interface RegistryConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MiscConfig {
|
|
||||||
projectDomain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
server: ServerConfig;
|
server: ServerConfig;
|
||||||
database: DatabaseConfig;
|
database: DatabaseConfig;
|
||||||
gitHub: GitHubConfig;
|
gitHub: GitHubConfig;
|
||||||
registryConfig: RegistryConfig;
|
registryConfig: RegistryConfig;
|
||||||
misc: MiscConfig;
|
|
||||||
turnkey: {
|
|
||||||
apiBaseUrl: string;
|
|
||||||
apiPublicKey: string;
|
|
||||||
apiPrivateKey: string;
|
|
||||||
defaultOrganizationId: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,11 @@ export const DEFAULT_CONFIG_FILE_PATH =
|
|||||||
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
|
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
|
||||||
|
|
||||||
export const DEFAULT_GQL_PATH = '/graphql';
|
export const DEFAULT_GQL_PATH = '/graphql';
|
||||||
|
|
||||||
|
// Note: temporary hardcoded user, later to be derived from auth token
|
||||||
|
export const USER_ID =
|
||||||
|
process.env.SNOWBALL_BACKEND_USER_ID ||
|
||||||
|
'59f4355d-9549-4aac-9b54-eeefceeabef0';
|
||||||
|
|
||||||
|
export const PROJECT_DOMAIN =
|
||||||
|
process.env.SNOWBALL_BACKEND_PROJECT_DOMAIN || 'snowball.xyz';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import assert from 'assert';
|
|||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import { lowercase, numbers } from 'nanoid-dictionary';
|
import { lowercase, numbers } from 'nanoid-dictionary';
|
||||||
|
|
||||||
import { DatabaseConfig, MiscConfig } from './config';
|
import { DatabaseConfig } from './config';
|
||||||
import { User } from './entity/User';
|
import { User } from './entity/User';
|
||||||
import { Organization } from './entity/Organization';
|
import { Organization } from './entity/Organization';
|
||||||
import { Project } from './entity/Project';
|
import { Project } from './entity/Project';
|
||||||
@@ -19,10 +19,13 @@ import { Deployment } from './entity/Deployment';
|
|||||||
import { ProjectMember } from './entity/ProjectMember';
|
import { ProjectMember } from './entity/ProjectMember';
|
||||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||||
import { Domain } from './entity/Domain';
|
import { Domain } from './entity/Domain';
|
||||||
|
import { PROJECT_DOMAIN } from './constants';
|
||||||
import { getEntities, loadAndSaveData } from './utils';
|
import { getEntities, loadAndSaveData } from './utils';
|
||||||
import { UserOrganization } from './entity/UserOrganization';
|
import { UserOrganization } from './entity/UserOrganization';
|
||||||
|
|
||||||
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
||||||
|
const USER_DATA_PATH = '../test/fixtures/users.json';
|
||||||
|
const USER_ORGANIZATION_DATA_PATH = '../test/fixtures/user-organizations.json';
|
||||||
|
|
||||||
const log = debug('snowball:database');
|
const log = debug('snowball:database');
|
||||||
|
|
||||||
@@ -31,9 +34,8 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
|
|||||||
// TODO: Fix order of methods
|
// TODO: Fix order of methods
|
||||||
export class Database {
|
export class Database {
|
||||||
private dataSource: DataSource;
|
private dataSource: DataSource;
|
||||||
private projectDomain: string;
|
|
||||||
|
|
||||||
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) {
|
constructor ({ dbPath }: DatabaseConfig) {
|
||||||
this.dataSource = new DataSource({
|
this.dataSource = new DataSource({
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
database: dbPath,
|
database: dbPath,
|
||||||
@@ -41,8 +43,6 @@ export class Database {
|
|||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: false
|
logging: false
|
||||||
});
|
});
|
||||||
|
|
||||||
this.projectDomain = projectDomain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init (): Promise<void> {
|
async init (): Promise<void> {
|
||||||
@@ -51,10 +51,36 @@ export class Database {
|
|||||||
|
|
||||||
const organizations = await this.getOrganizations({});
|
const organizations = await this.getOrganizations({});
|
||||||
|
|
||||||
// Load an organization if none exist
|
|
||||||
if (!organizations.length) {
|
if (!organizations.length) {
|
||||||
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
const orgEntities = await getEntities(
|
||||||
await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
|
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedOrgs = await loadAndSaveData(Organization, this.dataSource, [
|
||||||
|
orgEntities[0]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// TODO: Remove user once authenticated
|
||||||
|
const userEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, USER_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedUsers = await loadAndSaveData(User, this.dataSource, [
|
||||||
|
userEntities[0]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userOrganizationRelations = {
|
||||||
|
member: savedUsers,
|
||||||
|
organization: savedOrgs
|
||||||
|
};
|
||||||
|
|
||||||
|
const userOrgEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
UserOrganization,
|
||||||
|
this.dataSource,
|
||||||
|
[userOrgEntities[0]],
|
||||||
|
userOrganizationRelations
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,9 +98,9 @@ export class Database {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser (user: User, data: DeepPartial<User>): Promise<boolean> {
|
async updateUser (userId: string, data: DeepPartial<User>): Promise<boolean> {
|
||||||
const userRepository = this.dataSource.getRepository(User);
|
const userRepository = this.dataSource.getRepository(User);
|
||||||
const updateResult = await userRepository.update({ id: user.id }, data);
|
const updateResult = await userRepository.update({ id: userId }, data);
|
||||||
assert(updateResult.affected);
|
assert(updateResult.affected);
|
||||||
|
|
||||||
return updateResult.affected > 0;
|
return updateResult.affected > 0;
|
||||||
@@ -114,13 +140,6 @@ export class Database {
|
|||||||
return userOrgs;
|
return userOrgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addUserOrganization (data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
|
|
||||||
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
|
|
||||||
const newUserOrganization = await userOrganizationRepository.save(data);
|
|
||||||
|
|
||||||
return newUserOrganization;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProjects (options: FindManyOptions<Project>): Promise<Project[]> {
|
async getProjects (options: FindManyOptions<Project>): Promise<Project[]> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
const projects = await projectRepository.find(options);
|
const projects = await projectRepository.find(options);
|
||||||
@@ -436,20 +455,11 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteDeploymentById (deploymentId: string): Promise<boolean> {
|
async addProject (
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
userId: string,
|
||||||
const deployment = await deploymentRepository.findOneOrFail({
|
organizationId: string,
|
||||||
where: {
|
data: DeepPartial<Project>
|
||||||
id: deploymentId
|
): Promise<Project> {
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
// TODO: Check if organization exists
|
// TODO: Check if organization exists
|
||||||
@@ -459,13 +469,15 @@ export class Database {
|
|||||||
// TODO: Set icon according to framework
|
// TODO: Set icon according to framework
|
||||||
newProject.icon = '';
|
newProject.icon = '';
|
||||||
|
|
||||||
newProject.owner = user;
|
newProject.owner = Object.assign(new User(), {
|
||||||
|
id: userId
|
||||||
|
});
|
||||||
|
|
||||||
newProject.organization = Object.assign(new Organization(), {
|
newProject.organization = Object.assign(new Organization(), {
|
||||||
id: organizationId
|
id: organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
|
newProject.subDomain = `${newProject.name}.${PROJECT_DOMAIN}`;
|
||||||
|
|
||||||
return projectRepository.save(newProject);
|
return projectRepository.save(newProject);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
JoinColumn,
|
JoinColumn
|
||||||
DeleteDateColumn
|
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { Project } from './Project';
|
import { Project } from './Project';
|
||||||
import { Domain } from './Domain';
|
import { Domain } from './Domain';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
|
import { AppDeploymentRecordAttributes } from '../types';
|
||||||
|
|
||||||
export enum Environment {
|
export enum Environment {
|
||||||
Production = 'Production',
|
Production = 'Production',
|
||||||
@@ -25,7 +24,6 @@ export enum DeploymentStatus {
|
|||||||
Building = 'Building',
|
Building = 'Building',
|
||||||
Ready = 'Ready',
|
Ready = 'Ready',
|
||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
Deleting = 'Deleting',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApplicationDeploymentRequest {
|
export interface ApplicationDeploymentRequest {
|
||||||
@@ -37,18 +35,6 @@ export interface ApplicationDeploymentRequest {
|
|||||||
meta: string;
|
meta: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApplicationDeploymentRemovalRequest {
|
|
||||||
type: string;
|
|
||||||
version: string;
|
|
||||||
deployment: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApplicationDeploymentRemovalRequest {
|
|
||||||
type: string;
|
|
||||||
version: string;
|
|
||||||
deployment: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApplicationRecord {
|
export interface ApplicationRecord {
|
||||||
type: string;
|
type: string;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -101,11 +87,11 @@ export class Deployment {
|
|||||||
@Column('simple-json')
|
@Column('simple-json')
|
||||||
applicationRecordData!: ApplicationRecord;
|
applicationRecordData!: ApplicationRecord;
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
@Column('varchar')
|
||||||
applicationDeploymentRequestId!: string | null;
|
applicationDeploymentRequestId!: string;
|
||||||
|
|
||||||
@Column('simple-json', { nullable: true })
|
@Column('simple-json')
|
||||||
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
|
applicationDeploymentRequestData!: ApplicationDeploymentRequest;
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
applicationDeploymentRecordId!: string | null;
|
applicationDeploymentRecordId!: string | null;
|
||||||
@@ -113,18 +99,6 @@ export class Deployment {
|
|||||||
@Column('simple-json', { nullable: true })
|
@Column('simple-json', { nullable: true })
|
||||||
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
|
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({
|
@Column({
|
||||||
enum: Environment
|
enum: Environment
|
||||||
})
|
})
|
||||||
@@ -147,7 +121,4 @@ export class Deployment {
|
|||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@DeleteDateColumn()
|
|
||||||
deletedAt!: Date | null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,10 @@ import { UserOrganization } from './UserOrganization';
|
|||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Unique(['email'])
|
@Unique(['email'])
|
||||||
@Unique(['ethAddress'])
|
|
||||||
export class User {
|
export class User {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
// TODO: Set ethAddress as ID
|
|
||||||
@Column()
|
|
||||||
ethAddress!: string;
|
|
||||||
|
|
||||||
@Column('varchar', { length: 255, nullable: true })
|
@Column('varchar', { length: 255, nullable: true })
|
||||||
name!: string | null;
|
name!: string | null;
|
||||||
|
|
||||||
@@ -39,12 +34,6 @@ export class User {
|
|||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@Column()
|
|
||||||
subOrgId!: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
turnkeyWalletId!: string;
|
|
||||||
|
|
||||||
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||||
cascade: ['soft-remove']
|
cascade: ['soft-remove']
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { Organization } from './Organization';
|
import { Organization } from './Organization';
|
||||||
|
|
||||||
export enum Role {
|
enum Role {
|
||||||
Owner = 'Owner',
|
Owner = 'Owner',
|
||||||
Maintainer = 'Maintainer',
|
Maintainer = 'Maintainer',
|
||||||
Reader = 'Reader',
|
Reader = 'Reader',
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { Database } from './database';
|
|||||||
import { createAndStartServer } from './server';
|
import { createAndStartServer } from './server';
|
||||||
import { createResolvers } from './resolvers';
|
import { createResolvers } from './resolvers';
|
||||||
import { getConfig } from './utils';
|
import { getConfig } from './utils';
|
||||||
|
import { Config } from './config';
|
||||||
|
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
||||||
import { Service } from './service';
|
import { Service } from './service';
|
||||||
import { Registry } from './registry';
|
import { Registry } from './registry';
|
||||||
|
|
||||||
@@ -16,15 +18,18 @@ const log = debug('snowball:server');
|
|||||||
const OAUTH_CLIENT_TYPE = 'oauth-app';
|
const OAUTH_CLIENT_TYPE = 'oauth-app';
|
||||||
|
|
||||||
export const main = async (): Promise<void> => {
|
export const main = async (): Promise<void> => {
|
||||||
const { server, database, gitHub, registryConfig, misc } = await getConfig();
|
// TODO: get config path using cli
|
||||||
|
const { server, database, gitHub, registryConfig } = await getConfig<Config>(
|
||||||
|
DEFAULT_CONFIG_FILE_PATH
|
||||||
|
);
|
||||||
|
|
||||||
const app = new OAuthApp({
|
const app = new OAuthApp({
|
||||||
clientType: OAUTH_CLIENT_TYPE,
|
clientType: OAUTH_CLIENT_TYPE,
|
||||||
clientId: gitHub.oAuth.clientId,
|
clientId: gitHub.oAuth.clientId,
|
||||||
clientSecret: gitHub.oAuth.clientSecret,
|
clientSecret: gitHub.oAuth.clientSecret
|
||||||
});
|
});
|
||||||
|
|
||||||
const db = new Database(database, misc);
|
const db = new Database(database);
|
||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
const registry = new Registry(registryConfig);
|
const registry = new Registry(registryConfig);
|
||||||
@@ -32,7 +37,7 @@ export const main = async (): Promise<void> => {
|
|||||||
{ gitHubConfig: gitHub, registryConfig },
|
{ gitHubConfig: gitHub, registryConfig },
|
||||||
db,
|
db,
|
||||||
app,
|
app,
|
||||||
registry,
|
registry
|
||||||
);
|
);
|
||||||
|
|
||||||
const typeDefs = fs
|
const typeDefs = fs
|
||||||
|
|||||||
@@ -3,26 +3,21 @@ import assert from 'assert';
|
|||||||
import { inc as semverInc } from 'semver';
|
import { inc as semverInc } from 'semver';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk';
|
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
||||||
|
|
||||||
import { RegistryConfig } from './config';
|
import { RegistryConfig } from './config';
|
||||||
import {
|
import {
|
||||||
ApplicationRecord,
|
ApplicationRecord,
|
||||||
Deployment,
|
Deployment,
|
||||||
ApplicationDeploymentRequest,
|
ApplicationDeploymentRequest
|
||||||
ApplicationDeploymentRemovalRequest
|
|
||||||
} from './entity/Deployment';
|
} from './entity/Deployment';
|
||||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
|
import { AppDeploymentRecord, PackageJSON } from './types';
|
||||||
import { sleep } from './utils';
|
|
||||||
|
|
||||||
const log = debug('snowball:registry');
|
const log = debug('snowball:registry');
|
||||||
|
|
||||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
||||||
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
|
|
||||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
||||||
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
|
|
||||||
const SLEEP_DURATION = 1000;
|
|
||||||
|
|
||||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
// TODO: Move registry code to laconic-sdk/watcher-ts
|
||||||
export class Registry {
|
export class Registry {
|
||||||
@@ -113,24 +108,19 @@ export class Registry {
|
|||||||
log('Application record data:', applicationRecord);
|
log('Application record data:', applicationRecord);
|
||||||
|
|
||||||
// TODO: Discuss computation of CRN
|
// TODO: Discuss computation of CRN
|
||||||
const crn = this.getCrn(appName);
|
const crn = this.getCrn(packageJSON.name, appName);
|
||||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
|
||||||
await this.registry.setName(
|
await this.registry.setName(
|
||||||
{ cid: result.data.id, crn },
|
{ cid: result.data.id, crn },
|
||||||
this.registryConfig.privateKey,
|
this.registryConfig.privateKey,
|
||||||
this.registryConfig.fee
|
this.registryConfig.fee
|
||||||
);
|
);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
|
||||||
await this.registry.setName(
|
await this.registry.setName(
|
||||||
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
||||||
this.registryConfig.privateKey,
|
this.registryConfig.privateKey,
|
||||||
this.registryConfig.fee
|
this.registryConfig.fee
|
||||||
);
|
);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
|
||||||
await this.registry.setName(
|
await this.registry.setName(
|
||||||
{
|
{
|
||||||
cid: result.data.id,
|
cid: result.data.id,
|
||||||
@@ -147,16 +137,16 @@ export class Registry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationDeploymentRequest (data: {
|
async createApplicationDeploymentRequest (data: {
|
||||||
deployment: Deployment,
|
appName: string;
|
||||||
appName: string,
|
packageJsonName: string;
|
||||||
repository: string,
|
commitHash: string;
|
||||||
environmentVariables: { [key: string]: string },
|
repository: string;
|
||||||
dns: string,
|
environmentVariables: { [key: string]: string };
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationDeploymentRequestId: string;
|
applicationDeploymentRequestId: string;
|
||||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||||
}> {
|
}> {
|
||||||
const crn = this.getCrn(data.appName);
|
const crn = this.getCrn(data.packageJsonName, data.appName);
|
||||||
const records = await this.registry.resolveNames([crn]);
|
const records = await this.registry.resolveNames([crn]);
|
||||||
const applicationRecord = records[0];
|
const applicationRecord = records[0];
|
||||||
|
|
||||||
@@ -170,9 +160,9 @@ export class Registry {
|
|||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||||
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
||||||
dns: data.dns,
|
|
||||||
|
|
||||||
// TODO: Not set in test-progressive-web-app CI
|
// TODO: Not set in test-progressive-web-app CI
|
||||||
|
// dns: '$CERC_REGISTRY_DEPLOYMENT_SHORT_HOSTNAME',
|
||||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
||||||
|
|
||||||
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
||||||
@@ -184,11 +174,10 @@ export class Registry {
|
|||||||
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
|
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
|
||||||
)}`,
|
)}`,
|
||||||
repository: data.repository,
|
repository: data.repository,
|
||||||
repository_ref: data.deployment.commitHash
|
repository_ref: data.commitHash
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
|
||||||
const result = await this.registry.setRecord(
|
const result = await this.registry.setRecord(
|
||||||
{
|
{
|
||||||
privateKey: this.registryConfig.privateKey,
|
privateKey: this.registryConfig.privateKey,
|
||||||
@@ -222,86 +211,19 @@ export class Registry {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter records with ApplicationRecord ID and Deployment specific URL
|
// Filter records with ApplicationRecord ids
|
||||||
return records.filter((record: AppDeploymentRecord) =>
|
return records.filter((record: AppDeploymentRecord) =>
|
||||||
deployments.some(
|
deployments.some(
|
||||||
(deployment) =>
|
(deployment) =>
|
||||||
deployment.applicationRecordId === record.attributes.application &&
|
deployment.applicationRecordId === record.attributes.application
|
||||||
record.attributes.url.includes(deployment.id)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getCrn (packageJsonName: string, appName: string): string {
|
||||||
* Fetch ApplicationDeploymentRecords by filter
|
const [arg1] = packageJsonName.split('/');
|
||||||
*/
|
const authority = arg1.replace('@', '');
|
||||||
async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
|
||||||
return this.registry.queryRecords(
|
|
||||||
{
|
|
||||||
type: APP_DEPLOYMENT_RECORD_TYPE,
|
|
||||||
...filter
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return `crn://${authority}/applications/${appName}`;
|
||||||
* 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
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createApplicationDeploymentRemovalRequest (data: {
|
|
||||||
deploymentId: string;
|
|
||||||
}): Promise<{
|
|
||||||
applicationDeploymentRemovalRequestId: string;
|
|
||||||
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
|
||||||
}> {
|
|
||||||
const applicationDeploymentRemovalRequest = {
|
|
||||||
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
|
||||||
version: '1.0.0',
|
|
||||||
deployment: data.deploymentId
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await this.registry.setRecord(
|
|
||||||
{
|
|
||||||
privateKey: this.registryConfig.privateKey,
|
|
||||||
record: applicationDeploymentRemovalRequest,
|
|
||||||
bondId: this.registryConfig.bondId
|
|
||||||
},
|
|
||||||
'',
|
|
||||||
this.registryConfig.fee
|
|
||||||
);
|
|
||||||
|
|
||||||
log(`Application deployment removal request record published: ${result.data.id}`);
|
|
||||||
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
|
|
||||||
|
|
||||||
return {
|
|
||||||
applicationDeploymentRemovalRequestId: result.data.id,
|
|
||||||
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getCrn (appName: string): string {
|
|
||||||
assert(this.registryConfig.authority, "Authority doesn't exist");
|
|
||||||
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,26 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
Query: {
|
Query: {
|
||||||
// TODO: add custom type for context
|
// TODO: add custom type for context
|
||||||
user: (_: any, __: any, context: any) => {
|
user: (_: any, __: any, context: any) => {
|
||||||
return context.user;
|
return service.getUser(context.userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
organizations: async (_:any, __: any, context: any) => {
|
organizations: async (_: any, __: any, context: any) => {
|
||||||
return service.getOrganizationsByUserId(context.user);
|
return service.getOrganizationsByUserId(context.userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
project: async (_: any, { projectId }: { projectId: string }) => {
|
project: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
return service.getProjectById(projectId);
|
return service.getProjectById(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
|
projectsInOrganization: async (
|
||||||
return service.getProjectsInOrganization(context.user, organizationSlug);
|
_: any,
|
||||||
|
{ organizationSlug }: { organizationSlug: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
return service.getProjectsInOrganization(
|
||||||
|
context.userId,
|
||||||
|
organizationSlug
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
@@ -44,8 +51,12 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
return service.getProjectMembersByProjectId(projectId);
|
return service.getProjectMembersByProjectId(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
|
searchProjects: async (
|
||||||
return service.searchProjects(context.user, searchText);
|
_: any,
|
||||||
|
{ searchText }: { searchText: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
|
return service.searchProjects(context.userId, searchText);
|
||||||
},
|
},
|
||||||
|
|
||||||
domains: async (
|
domains: async (
|
||||||
@@ -67,7 +78,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
context: any
|
context: any
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.removeProjectMember(context.user, projectMemberId);
|
return await service.removeProjectMember(
|
||||||
|
context.userId,
|
||||||
|
projectMemberId
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -174,7 +188,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
context: any
|
context: any
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
|
return Boolean(
|
||||||
|
await service.updateDeploymentToProd(context.userId, deploymentId)
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -190,7 +206,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
context: any
|
context: any
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.addProject(context.user, organizationSlug, data);
|
return await service.addProject(
|
||||||
|
context.userId,
|
||||||
|
organizationSlug,
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -215,7 +235,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
context: any
|
context: any
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.redeployToProd(context.user, deploymentId));
|
return Boolean(
|
||||||
|
await service.redeployToProd(context.userId, deploymentId)
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -255,20 +277,6 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteDeployment: async (
|
|
||||||
_: any,
|
|
||||||
{
|
|
||||||
deploymentId
|
|
||||||
}: { deploymentId: string; }
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
return await service.deleteDeployment(deploymentId);
|
|
||||||
} catch (err) {
|
|
||||||
log(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addDomain: async (
|
addDomain: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ projectId, data }: { projectId: string; data: { name: string } }
|
{ projectId, data }: { projectId: string; data: { name: string } }
|
||||||
@@ -299,7 +307,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
context: any
|
context: any
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.authenticateGitHub(code, context.user);
|
return await service.authenticateGitHub(code, context.userId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -308,7 +316,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
|
|
||||||
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||||
try {
|
try {
|
||||||
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
|
return service.unauthenticateGitHub(context.userId, {
|
||||||
|
gitHubToken: null
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import { SiweMessage } from 'siwe';
|
|
||||||
import { Service } from '../service';
|
|
||||||
import { authenticateUser, createUser } from '../turnkey-backend';
|
|
||||||
|
|
||||||
const 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 });
|
|
||||||
} else {
|
|
||||||
return res.sendStatus(204);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/register', async (req, res) => {
|
|
||||||
const { email, challenge, attestation } = req.body;
|
|
||||||
const user = await createUser(req.app.get('service'), {
|
|
||||||
challenge,
|
|
||||||
attestation,
|
|
||||||
userEmail: email,
|
|
||||||
userName: email.split('@')[0],
|
|
||||||
});
|
|
||||||
req.session.userId = user.id;
|
|
||||||
res.sendStatus(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/authenticate', async (req, res) => {
|
|
||||||
const { signedWhoamiRequest } = req.body;
|
|
||||||
const user = await authenticateUser(
|
|
||||||
req.app.get('service'),
|
|
||||||
signedWhoamiRequest,
|
|
||||||
);
|
|
||||||
if (user) {
|
|
||||||
req.session.userId = user.id;
|
|
||||||
res.sendStatus(200);
|
|
||||||
} else {
|
|
||||||
res.sendStatus(401);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lit
|
|
||||||
//
|
|
||||||
|
|
||||||
router.post('/validate', async (req, res) => {
|
|
||||||
const { message, signature, action } = 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 (action === 'signup') {
|
|
||||||
if (user) {
|
|
||||||
return res.send({ success: false, error: 'user_already_exists' });
|
|
||||||
}
|
|
||||||
const newUser = await service.createUser({
|
|
||||||
ethAddress: data.address,
|
|
||||||
email: '',
|
|
||||||
name: '',
|
|
||||||
subOrgId: '',
|
|
||||||
turnkeyWalletId: '',
|
|
||||||
});
|
|
||||||
req.session.userId = newUser.id;
|
|
||||||
} else if (action === 'login') {
|
|
||||||
if (!user) {
|
|
||||||
return res.send({ success: false, error: 'user_not_found' });
|
|
||||||
}
|
|
||||||
req.session.userId = user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send({ success });
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// General
|
|
||||||
//
|
|
||||||
router.get('/session', (req, res) => {
|
|
||||||
if (req.session.userId) {
|
|
||||||
res.send({
|
|
||||||
userId: req.session.userId,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.status(401).send({ error: 'Unauthorized: No active session' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/logout', (req, res) => {
|
|
||||||
// This is how you clear cookie-session
|
|
||||||
(req as any).session = null;
|
|
||||||
res.send({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -19,7 +19,6 @@ enum DeploymentStatus {
|
|||||||
Building
|
Building
|
||||||
Ready
|
Ready
|
||||||
Error
|
Error
|
||||||
Deleting
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DomainStatus {
|
enum DomainStatus {
|
||||||
@@ -210,7 +209,6 @@ type Mutation {
|
|||||||
deleteProject(projectId: String!): Boolean!
|
deleteProject(projectId: String!): Boolean!
|
||||||
deleteDomain(domainId: String!): Boolean!
|
deleteDomain(domainId: String!): Boolean!
|
||||||
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
|
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
|
||||||
deleteDeployment(deploymentId: String!): Boolean!
|
|
||||||
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
||||||
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
||||||
authenticateGitHub(code: String!): AuthResult!
|
authenticateGitHub(code: String!): AuthResult!
|
||||||
|
|||||||
@@ -1,40 +1,29 @@
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cors from 'cors';
|
|
||||||
import { ApolloServer } from 'apollo-server-express';
|
import { ApolloServer } from 'apollo-server-express';
|
||||||
import { createServer } from 'http';
|
import { createServer } from 'http';
|
||||||
import {
|
import {
|
||||||
ApolloServerPluginDrainHttpServer,
|
ApolloServerPluginDrainHttpServer,
|
||||||
ApolloServerPluginLandingPageLocalDefault,
|
ApolloServerPluginLandingPageLocalDefault
|
||||||
AuthenticationError,
|
|
||||||
} from 'apollo-server-core';
|
} from 'apollo-server-core';
|
||||||
import cookieSession from 'cookie-session';
|
|
||||||
|
|
||||||
import { TypeSource } from '@graphql-tools/utils';
|
import { TypeSource } from '@graphql-tools/utils';
|
||||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||||
|
|
||||||
import { ServerConfig } from './config';
|
import { ServerConfig } from './config';
|
||||||
import { DEFAULT_GQL_PATH } from './constants';
|
import { DEFAULT_GQL_PATH, USER_ID } from './constants';
|
||||||
import githubRouter from './routes/github';
|
import githubRouter from './routes/github';
|
||||||
import authRouter from './routes/auth';
|
|
||||||
import { Service } from './service';
|
import { Service } from './service';
|
||||||
|
|
||||||
const log = debug('snowball:server');
|
const log = debug('snowball:server');
|
||||||
|
|
||||||
declare module 'express-session' {
|
|
||||||
interface SessionData {
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createAndStartServer = async (
|
export const createAndStartServer = async (
|
||||||
serverConfig: ServerConfig,
|
serverConfig: ServerConfig,
|
||||||
typeDefs: TypeSource,
|
typeDefs: TypeSource,
|
||||||
resolvers: any,
|
resolvers: any,
|
||||||
service: Service,
|
service: Service
|
||||||
): Promise<ApolloServer> => {
|
): Promise<ApolloServer> => {
|
||||||
const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig;
|
const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig;
|
||||||
const { appOriginUrl, secret, domain, trustProxy } = serverConfig.session;
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -44,70 +33,32 @@ export const createAndStartServer = async (
|
|||||||
// Create the schema
|
// Create the schema
|
||||||
const schema = makeExecutableSchema({
|
const schema = makeExecutableSchema({
|
||||||
typeDefs,
|
typeDefs,
|
||||||
resolvers,
|
resolvers
|
||||||
});
|
});
|
||||||
|
|
||||||
const server = new ApolloServer({
|
const server = new ApolloServer({
|
||||||
schema,
|
schema,
|
||||||
csrfPrevention: true,
|
csrfPrevention: true,
|
||||||
context: async ({ req }) => {
|
context: () => {
|
||||||
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
// TODO: Use userId derived from auth token
|
||||||
|
return { userId: USER_ID };
|
||||||
const { userId } = req.session;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new AuthenticationError('Unauthorized: No active session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await service.getUser(userId);
|
|
||||||
|
|
||||||
return { user };
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// Proper shutdown for the HTTP server
|
// Proper shutdown for the HTTP server
|
||||||
ApolloServerPluginDrainHttpServer({ httpServer }),
|
ApolloServerPluginDrainHttpServer({ httpServer }),
|
||||||
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
|
ApolloServerPluginLandingPageLocalDefault({ embed: true })
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|
||||||
app.use(
|
|
||||||
cors({
|
|
||||||
origin: appOriginUrl,
|
|
||||||
credentials: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (trustProxy) {
|
|
||||||
// trust first proxy
|
|
||||||
app.set('trust proxy', 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
cookieSession({
|
|
||||||
secret: secret,
|
|
||||||
secure: new URL(appOriginUrl).protocol === 'https:',
|
|
||||||
// 23 hours (less than 24 hours to avoid sessionSigs expiration issues)
|
|
||||||
maxAge: 23 * 60 * 60 * 1000,
|
|
||||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
|
||||||
domain: domain || undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
server.applyMiddleware({
|
server.applyMiddleware({
|
||||||
app,
|
app,
|
||||||
path: gqlPath,
|
path: gqlPath
|
||||||
cors: {
|
|
||||||
origin: [appOriginUrl],
|
|
||||||
credentials: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
app.set('service', service);
|
app.set('service', service);
|
||||||
app.use('/auth', authRouter);
|
app.use(express.json());
|
||||||
app.use('/api/github', githubRouter);
|
app.use('/api/github', githubRouter);
|
||||||
|
|
||||||
httpServer.listen(port, host, () => {
|
httpServer.listen(port, host, () => {
|
||||||
|
|||||||
@@ -15,16 +15,12 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
|
|||||||
import { User } from './entity/User';
|
import { User } from './entity/User';
|
||||||
import { Registry } from './registry';
|
import { Registry } from './registry';
|
||||||
import { GitHubConfig, RegistryConfig } from './config';
|
import { GitHubConfig, RegistryConfig } from './config';
|
||||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, GitPushEventPayload, PackageJSON } from './types';
|
import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
|
||||||
import { Role } from './entity/UserOrganization';
|
|
||||||
|
|
||||||
const log = debug('snowball:service');
|
const log = debug('snowball:service');
|
||||||
|
|
||||||
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
||||||
|
|
||||||
// Define a constant for an hour in milliseconds
|
|
||||||
const HOUR = 1000 * 60 * 60;
|
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
gitHubConfig: GitHubConfig;
|
gitHubConfig: GitHubConfig;
|
||||||
registryConfig: RegistryConfig;
|
registryConfig: RegistryConfig;
|
||||||
@@ -52,8 +48,6 @@ export class Service {
|
|||||||
init (): void {
|
init (): void {
|
||||||
// Start check for ApplicationDeploymentRecords asynchronously
|
// Start check for ApplicationDeploymentRecords asynchronously
|
||||||
this.checkDeployRecordsAndUpdate();
|
this.checkDeployRecordsAndUpdate();
|
||||||
// Start check for ApplicationDeploymentRemovalRecords asynchronously
|
|
||||||
this.checkDeploymentRemovalRecordsAndUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,13 +59,14 @@ export class Service {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for ApplicationDeploymentRecord and update corresponding deployments
|
* Checks for ApplicationDeploymentRecord and update corresponding deployments
|
||||||
* Continues check in loop after a delay of registryConfig.fetchDeploymentRecordDelay
|
* Continues check in loop after a delay of DEPLOY_RECORD_CHECK_DELAY_MS
|
||||||
*/
|
*/
|
||||||
async checkDeployRecordsAndUpdate (): Promise<void> {
|
async checkDeployRecordsAndUpdate (): Promise<void> {
|
||||||
// Fetch deployments in building state
|
// Fetch deployments in building state
|
||||||
const deployments = await this.db.getDeployments({
|
const deployments = await this.db.getDeployments({
|
||||||
where: {
|
where: {
|
||||||
status: DeploymentStatus.Building
|
status: DeploymentStatus.Building
|
||||||
|
// TODO: Fetch and check records for recent deployments
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,28 +75,6 @@ export class Service {
|
|||||||
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
|
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate a timestamp for one hour ago
|
|
||||||
const anHourAgo = Date.now() - HOUR;
|
|
||||||
|
|
||||||
// Filter out deployments started more than an hour ago and mark them as Error
|
|
||||||
const oldDeploymentsToUpdate = deployments.filter(
|
|
||||||
deployment => (Number(deployment.updatedAt) < anHourAgo)
|
|
||||||
)
|
|
||||||
.map((deployment) => {
|
|
||||||
return this.db.updateDeploymentById(deployment.id, {
|
|
||||||
status: DeploymentStatus.Error,
|
|
||||||
isCurrent: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there are old deployments to update, log and perform the updates
|
|
||||||
if (oldDeploymentsToUpdate.length > 0) {
|
|
||||||
log(
|
|
||||||
`Cleaning up ${oldDeploymentsToUpdate.length} deployments stuck in ${DeploymentStatus.Building} state for over an hour`
|
|
||||||
);
|
|
||||||
await Promise.all(oldDeploymentsToUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch ApplicationDeploymentRecord for deployments
|
// Fetch ApplicationDeploymentRecord for deployments
|
||||||
const records = await this.registry.getDeploymentRecords(deployments);
|
const records = await this.registry.getDeploymentRecords(deployments);
|
||||||
log(`Found ${records.length} ApplicationDeploymentRecords`);
|
log(`Found ${records.length} ApplicationDeploymentRecords`);
|
||||||
@@ -117,38 +90,6 @@ export class Service {
|
|||||||
}, this.config.registryConfig.fetchDeploymentRecordDelay);
|
}, this.config.registryConfig.fetchDeploymentRecordDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks for ApplicationDeploymentRemovalRecord and remove corresponding deployments
|
|
||||||
* Continues check in loop after a delay of registryConfig.fetchDeploymentRecordDelay
|
|
||||||
*/
|
|
||||||
async checkDeploymentRemovalRecordsAndUpdate (): Promise<void> {
|
|
||||||
// Fetch deployments in deleting state
|
|
||||||
const deployments = await this.db.getDeployments({
|
|
||||||
where: {
|
|
||||||
status: DeploymentStatus.Deleting
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (deployments.length) {
|
|
||||||
log(
|
|
||||||
`Found ${deployments.length} deployments in ${DeploymentStatus.Deleting} state`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch ApplicationDeploymentRemovalRecords for deployments
|
|
||||||
const records = await this.registry.getDeploymentRemovalRecords(deployments);
|
|
||||||
log(`Found ${records.length} ApplicationDeploymentRemovalRecords`);
|
|
||||||
|
|
||||||
// Update deployments for which ApplicationDeploymentRemovalRecords were returned
|
|
||||||
if (records.length) {
|
|
||||||
await this.deleteDeploymentsWithRecordData(records, deployments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.deployRecordCheckTimeout = setTimeout(() => {
|
|
||||||
this.checkDeploymentRemovalRecordsAndUpdate();
|
|
||||||
}, this.config.registryConfig.fetchDeploymentRecordDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update deployments with ApplicationDeploymentRecord data
|
* Update deployments with ApplicationDeploymentRecord data
|
||||||
*/
|
*/
|
||||||
@@ -211,45 +152,6 @@ export class Service {
|
|||||||
await Promise.all(deploymentUpdatePromises);
|
await Promise.all(deploymentUpdatePromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete deployments with ApplicationDeploymentRemovalRecord data
|
|
||||||
*/
|
|
||||||
async deleteDeploymentsWithRecordData (
|
|
||||||
records: AppDeploymentRemovalRecord[],
|
|
||||||
deployments: Deployment[],
|
|
||||||
): Promise<void> {
|
|
||||||
const removedApplicationDeploymentRecordIds = records.map(record => record.attributes.deployment);
|
|
||||||
|
|
||||||
// Get removed deployments for ApplicationDeploymentRecords
|
|
||||||
const removedDeployments = deployments.filter(deployment => removedApplicationDeploymentRecordIds.includes(deployment.applicationDeploymentRecordId!))
|
|
||||||
|
|
||||||
const recordToDeploymentsMap = removedDeployments.reduce(
|
|
||||||
(acc: { [key: string]: Deployment }, deployment) => {
|
|
||||||
acc[deployment.applicationDeploymentRecordId!] = deployment;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update deployment data for ApplicationDeploymentRecords and delete
|
|
||||||
const deploymentUpdatePromises = records.map(async (record) => {
|
|
||||||
const deployment = recordToDeploymentsMap[record.attributes.deployment];
|
|
||||||
|
|
||||||
await this.db.updateDeploymentById(deployment.id, {
|
|
||||||
applicationDeploymentRemovalRecordId: record.id,
|
|
||||||
applicationDeploymentRemovalRecordData: record.attributes,
|
|
||||||
});
|
|
||||||
|
|
||||||
log(
|
|
||||||
`Updated deployment ${deployment.id} with ApplicationDeploymentRemovalRecord ${record.id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.db.deleteDeploymentById(deployment.id)
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(deploymentUpdatePromises);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUser (userId: string): Promise<User | null> {
|
async getUser (userId: string): Promise<User | null> {
|
||||||
return this.db.getUser({
|
return this.db.getUser({
|
||||||
where: {
|
where: {
|
||||||
@@ -258,59 +160,6 @@ export class Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserByEmail(email: string): Promise<User | null> {
|
|
||||||
return await this.db.getUser({
|
|
||||||
where: {
|
|
||||||
email
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserBySubOrgId(subOrgId: string): Promise<User | null> {
|
|
||||||
return await this.db.getUser({
|
|
||||||
where: {
|
|
||||||
subOrgId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserByEthAddress (ethAddress: string): Promise<User | null> {
|
|
||||||
return await this.db.getUser({
|
|
||||||
where: {
|
|
||||||
ethAddress
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createUser (params: {
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
subOrgId: string
|
|
||||||
ethAddress: string
|
|
||||||
turnkeyWalletId: string
|
|
||||||
}): Promise<User> {
|
|
||||||
const [org] = await this.db.getOrganizations({});
|
|
||||||
assert(org, 'No organizations exists in database');
|
|
||||||
|
|
||||||
// Create user with new address
|
|
||||||
const user = await this.db.addUser({
|
|
||||||
email: params.email,
|
|
||||||
name: params.name,
|
|
||||||
subOrgId: params.subOrgId,
|
|
||||||
ethAddress: params.ethAddress,
|
|
||||||
isVerified: true,
|
|
||||||
turnkeyWalletId: params.turnkeyWalletId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.db.addUserOrganization({
|
|
||||||
member: user,
|
|
||||||
organization: org,
|
|
||||||
role: Role.Owner
|
|
||||||
});
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOctokit (userId: string): Promise<Octokit> {
|
async getOctokit (userId: string): Promise<Octokit> {
|
||||||
const user = await this.db.getUser({ where: { id: userId } });
|
const user = await this.db.getUser({ where: { id: userId } });
|
||||||
assert(
|
assert(
|
||||||
@@ -321,8 +170,8 @@ export class Service {
|
|||||||
return new Octokit({ auth: user.gitHubToken });
|
return new Octokit({ auth: user.gitHubToken });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizationsByUserId (user: User): Promise<Organization[]> {
|
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
||||||
const dbOrganizations = await this.db.getOrganizationsByUserId(user.id);
|
const dbOrganizations = await this.db.getOrganizationsByUserId(userId);
|
||||||
return dbOrganizations;
|
return dbOrganizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,8 +180,14 @@ export class Service {
|
|||||||
return dbProject;
|
return dbProject;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsInOrganization (user: User, organizationSlug: string): Promise<Project[]> {
|
async getProjectsInOrganization (
|
||||||
const dbProjects = await this.db.getProjectsInOrganization(user.id, organizationSlug);
|
userId: string,
|
||||||
|
organizationSlug: string
|
||||||
|
): Promise<Project[]> {
|
||||||
|
const dbProjects = await this.db.getProjectsInOrganization(
|
||||||
|
userId,
|
||||||
|
organizationSlug
|
||||||
|
);
|
||||||
return dbProjects;
|
return dbProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,8 +212,11 @@ export class Service {
|
|||||||
return dbProjectMembers;
|
return dbProjectMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchProjects (user: User, searchText: string): Promise<Project[]> {
|
async searchProjects (userId: string, searchText: string): Promise<Project[]> {
|
||||||
const dbProjects = await this.db.getProjectsBySearchText(user.id, searchText);
|
const dbProjects = await this.db.getProjectsBySearchText(
|
||||||
|
userId,
|
||||||
|
searchText
|
||||||
|
);
|
||||||
return dbProjects;
|
return dbProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,17 +269,20 @@ export class Service {
|
|||||||
return newProjectMember;
|
return newProjectMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeProjectMember (user: User, projectMemberId: string): Promise<boolean> {
|
async removeProjectMember (
|
||||||
|
userId: string,
|
||||||
|
projectMemberId: string
|
||||||
|
): Promise<boolean> {
|
||||||
const member = await this.db.getProjectMemberById(projectMemberId);
|
const member = await this.db.getProjectMemberById(projectMemberId);
|
||||||
|
|
||||||
if (String(member.member.id) === user.id) {
|
if (String(member.member.id) === userId) {
|
||||||
throw new Error('Invalid operation: cannot remove self');
|
throw new Error('Invalid operation: cannot remove self');
|
||||||
}
|
}
|
||||||
|
|
||||||
const memberProject = member.project;
|
const memberProject = member.project;
|
||||||
assert(memberProject);
|
assert(memberProject);
|
||||||
|
|
||||||
if (String(user.id) === String(memberProject.owner.id)) {
|
if (String(userId) === String(memberProject.owner.id)) {
|
||||||
return this.db.removeProjectMemberById(projectMemberId);
|
return this.db.removeProjectMemberById(projectMemberId);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid operation: not authorized');
|
throw new Error('Invalid operation: not authorized');
|
||||||
@@ -466,7 +327,10 @@ export class Service {
|
|||||||
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentToProd (user: User, deploymentId: string): Promise<Deployment> {
|
async updateDeploymentToProd (
|
||||||
|
userId: string,
|
||||||
|
deploymentId: string
|
||||||
|
): Promise<Deployment> {
|
||||||
const oldDeployment = await this.db.getDeployment({
|
const oldDeployment = await this.db.getDeployment({
|
||||||
where: { id: deploymentId },
|
where: { id: deploymentId },
|
||||||
relations: {
|
relations: {
|
||||||
@@ -483,11 +347,9 @@ export class Service {
|
|||||||
{ branch: oldDeployment.project.prodBranch }
|
{ branch: oldDeployment.project.prodBranch }
|
||||||
);
|
);
|
||||||
|
|
||||||
const octokit = await this.getOctokit(user.id);
|
const octokit = await this.getOctokit(userId);
|
||||||
|
|
||||||
const newDeployment = await this.createDeployment(user.id,
|
const newDeployment = await this.createDeployment(userId, octokit, {
|
||||||
octokit,
|
|
||||||
{
|
|
||||||
project: oldDeployment.project,
|
project: oldDeployment.project,
|
||||||
branch: oldDeployment.branch,
|
branch: oldDeployment.branch,
|
||||||
environment: Environment.Production,
|
environment: Environment.Production,
|
||||||
@@ -502,7 +364,8 @@ export class Service {
|
|||||||
async createDeployment (
|
async createDeployment (
|
||||||
userId: string,
|
userId: string,
|
||||||
octokit: Octokit,
|
octokit: Octokit,
|
||||||
data: DeepPartial<Deployment>
|
data: DeepPartial<Deployment>,
|
||||||
|
recordData: { repoUrl?: string } = {}
|
||||||
): Promise<Deployment> {
|
): Promise<Deployment> {
|
||||||
assert(data.project?.repository, 'Project repository not found');
|
assert(data.project?.repository, 'Project repository not found');
|
||||||
log(
|
log(
|
||||||
@@ -526,10 +389,13 @@ export class Service {
|
|||||||
|
|
||||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||||
|
|
||||||
const repoUrl = (await octokit.rest.repos.get({
|
if (!recordData.repoUrl) {
|
||||||
|
const { data: repoDetails } = await octokit.rest.repos.get({
|
||||||
owner,
|
owner,
|
||||||
repo
|
repo
|
||||||
})).data.html_url;
|
});
|
||||||
|
recordData.repoUrl = repoDetails.html_url;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
||||||
const { applicationRecordId, applicationRecordData } =
|
const { applicationRecordId, applicationRecordData } =
|
||||||
@@ -538,18 +404,42 @@ export class Service {
|
|||||||
packageJSON,
|
packageJSON,
|
||||||
appType: data.project!.template!,
|
appType: data.project!.template!,
|
||||||
commitHash: data.commitHash!,
|
commitHash: data.commitHash!,
|
||||||
repoUrl
|
repoUrl: recordData.repoUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
const environmentVariables =
|
||||||
|
await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
|
||||||
|
environment: Environment.Production
|
||||||
|
});
|
||||||
|
|
||||||
|
const environmentVariablesObj = environmentVariables.reduce(
|
||||||
|
(acc, env) => {
|
||||||
|
acc[env.key] = env.value;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as { [key: string]: string }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
|
||||||
|
await this.registry.createApplicationDeploymentRequest({
|
||||||
|
appName: repo,
|
||||||
|
packageJsonName: packageJSON.name,
|
||||||
|
commitHash: data.commitHash!,
|
||||||
|
repository: recordData.repoUrl,
|
||||||
|
environmentVariables: environmentVariablesObj
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update previous deployment with prod branch domain
|
// Update previous deployment with prod branch domain
|
||||||
// TODO: Fix unique constraint error for domain
|
// TODO: Fix unique constraint error for domain
|
||||||
if (data.domain) {
|
await this.db.updateDeployment(
|
||||||
await this.db.updateDeployment({
|
{
|
||||||
domainId: data.domain.id
|
domainId: data.domain?.id
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
domain: null
|
domain: null
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const newDeployment = await this.db.addDeployment({
|
const newDeployment = await this.db.addDeployment({
|
||||||
project: data.project,
|
project: data.project,
|
||||||
@@ -560,51 +450,25 @@ export class Service {
|
|||||||
status: DeploymentStatus.Building,
|
status: DeploymentStatus.Building,
|
||||||
applicationRecordId,
|
applicationRecordId,
|
||||||
applicationRecordData,
|
applicationRecordData,
|
||||||
|
applicationDeploymentRequestId,
|
||||||
|
applicationDeploymentRequestData,
|
||||||
domain: data.domain,
|
domain: data.domain,
|
||||||
createdBy: Object.assign(new User(), {
|
createdBy: Object.assign(new User(), {
|
||||||
id: userId
|
id: userId
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`);
|
log(
|
||||||
|
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`
|
||||||
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(data.project.id!, { environment: Environment.Production });
|
);
|
||||||
|
|
||||||
const environmentVariablesObj = environmentVariables.reduce((acc, env) => {
|
|
||||||
acc[env.key] = env.value;
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {} as { [key: string]: string });
|
|
||||||
|
|
||||||
// To set project DNS
|
|
||||||
if (data.environment === Environment.Production) {
|
|
||||||
// On deleting deployment later, project DNS deployment is also deleted
|
|
||||||
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
|
|
||||||
await this.registry.createApplicationDeploymentRequest(
|
|
||||||
{
|
|
||||||
deployment: newDeployment,
|
|
||||||
appName: repo,
|
|
||||||
repository: repoUrl,
|
|
||||||
environmentVariables: environmentVariablesObj,
|
|
||||||
dns: `${newDeployment.project.name}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
|
|
||||||
{
|
|
||||||
deployment: newDeployment,
|
|
||||||
appName: repo,
|
|
||||||
repository: repoUrl,
|
|
||||||
environmentVariables: environmentVariablesObj,
|
|
||||||
dns: `${newDeployment.project.name}-${newDeployment.id}`
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
|
|
||||||
|
|
||||||
return newDeployment;
|
return newDeployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProject (user: User, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
|
async addProject (
|
||||||
|
userId: string,
|
||||||
|
organizationSlug: string,
|
||||||
|
data: DeepPartial<Project>
|
||||||
|
): Promise<Project | undefined> {
|
||||||
const organization = await this.db.getOrganization({
|
const organization = await this.db.getOrganization({
|
||||||
where: {
|
where: {
|
||||||
slug: organizationSlug
|
slug: organizationSlug
|
||||||
@@ -614,9 +478,9 @@ export class Service {
|
|||||||
throw new Error('Organization does not exist');
|
throw new Error('Organization does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await this.db.addProject(user, organization.id, data);
|
const project = await this.db.addProject(userId, organization.id, data);
|
||||||
|
|
||||||
const octokit = await this.getOctokit(user.id);
|
const octokit = await this.getOctokit(userId);
|
||||||
const [owner, repo] = project.repository.split('/');
|
const [owner, repo] = project.repository.split('/');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -628,8 +492,11 @@ export class Service {
|
|||||||
per_page: 1
|
per_page: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
||||||
|
|
||||||
// Create deployment with prod branch and latest commit
|
// Create deployment with prod branch and latest commit
|
||||||
await this.createDeployment(user.id,
|
await this.createDeployment(
|
||||||
|
userId,
|
||||||
octokit,
|
octokit,
|
||||||
{
|
{
|
||||||
project,
|
project,
|
||||||
@@ -638,6 +505,9 @@ export class Service {
|
|||||||
domain: null,
|
domain: null,
|
||||||
commitHash: latestCommit.sha,
|
commitHash: latestCommit.sha,
|
||||||
commitMessage: latestCommit.commit.message
|
commitMessage: latestCommit.commit.message
|
||||||
|
},
|
||||||
|
{
|
||||||
|
repoUrl: repoDetails.html_url
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -680,14 +550,8 @@ export class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
||||||
const { repository, ref, head_commit: headCommit, deleted } = data;
|
const { repository, ref, head_commit: headCommit } = data;
|
||||||
|
log(`Handling GitHub push event from repository: ${repository.full_name}`);
|
||||||
if (deleted) {
|
|
||||||
log(`Branch ${ref} deleted for project ${repository.full_name}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`Handling GitHub push event from repository: ${repository.full_name}, branch: ${ref}`);
|
|
||||||
const projects = await this.db.getProjects({
|
const projects = await this.db.getProjects({
|
||||||
where: { repository: repository.full_name }
|
where: { repository: repository.full_name }
|
||||||
});
|
});
|
||||||
@@ -749,7 +613,10 @@ export class Service {
|
|||||||
return this.db.deleteDomainById(domainId);
|
return this.db.deleteDomainById(domainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async redeployToProd (user: User, deploymentId: string): Promise<Deployment> {
|
async redeployToProd (
|
||||||
|
userId: string,
|
||||||
|
deploymentId: string
|
||||||
|
): Promise<Deployment> {
|
||||||
const oldDeployment = await this.db.getDeployment({
|
const oldDeployment = await this.db.getDeployment({
|
||||||
relations: {
|
relations: {
|
||||||
project: true,
|
project: true,
|
||||||
@@ -765,11 +632,9 @@ export class Service {
|
|||||||
throw new Error('Deployment not found');
|
throw new Error('Deployment not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const octokit = await this.getOctokit(user.id);
|
const octokit = await this.getOctokit(userId);
|
||||||
|
|
||||||
const newDeployment = await this.createDeployment(user.id,
|
const newDeployment = await this.createDeployment(userId, octokit, {
|
||||||
octokit,
|
|
||||||
{
|
|
||||||
project: oldDeployment.project,
|
project: oldDeployment.project,
|
||||||
// TODO: Put isCurrent field in project
|
// TODO: Put isCurrent field in project
|
||||||
branch: oldDeployment.branch,
|
branch: oldDeployment.branch,
|
||||||
@@ -816,51 +681,6 @@ export class Service {
|
|||||||
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
|
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteDeployment (deploymentId: string): Promise<boolean> {
|
|
||||||
const deployment = await this.db.getDeployment({
|
|
||||||
where: {
|
|
||||||
id: deploymentId
|
|
||||||
},
|
|
||||||
relations: {
|
|
||||||
project: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (deployment && deployment.applicationDeploymentRecordId) {
|
|
||||||
// If deployment is current, remove deployment for project subdomain as well
|
|
||||||
if (deployment.isCurrent) {
|
|
||||||
const currentDeploymentURL = `https://${deployment.project.subDomain}`;
|
|
||||||
|
|
||||||
const deploymentRecords = await this.registry.getDeploymentRecordsByFilter({
|
|
||||||
application: deployment.applicationRecordId,
|
|
||||||
url: currentDeploymentURL
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!deploymentRecords.length) {
|
|
||||||
log(`No ApplicationDeploymentRecord found for URL ${currentDeploymentURL} and ApplicationDeploymentRecord id ${deployment.applicationDeploymentRecordId}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deploymentRecords[0].id });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deployment.applicationDeploymentRecordId });
|
|
||||||
|
|
||||||
await this.db.updateDeploymentById(
|
|
||||||
deployment.id,
|
|
||||||
{
|
|
||||||
status: DeploymentStatus.Deleting,
|
|
||||||
applicationDeploymentRemovalRequestId: result.applicationDeploymentRemovalRequestId,
|
|
||||||
applicationDeploymentRemovalRequestData: result.applicationDeploymentRemovalRequestData
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (result !== undefined || result !== null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addDomain (
|
async addDomain (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
data: { name: string }
|
data: { name: string }
|
||||||
@@ -958,17 +778,25 @@ export class Service {
|
|||||||
return updateResult;
|
return updateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticateGitHub (code:string, user: User): Promise<{token: string}> {
|
async authenticateGitHub (
|
||||||
const { authentication: { token } } = await this.oauthApp.createToken({
|
code: string,
|
||||||
|
userId: string
|
||||||
|
): Promise<{ token: string }> {
|
||||||
|
const {
|
||||||
|
authentication: { token }
|
||||||
|
} = await this.oauthApp.createToken({
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.db.updateUser(user, { gitHubToken: token });
|
await this.db.updateUser(userId, { gitHubToken: token });
|
||||||
|
|
||||||
return { token };
|
return { token };
|
||||||
}
|
}
|
||||||
|
|
||||||
async unauthenticateGitHub (user: User, data: DeepPartial<User>): Promise<boolean> {
|
async unauthenticateGitHub (
|
||||||
return this.db.updateUser(user, data);
|
userId: string,
|
||||||
|
data: DeepPartial<User>
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.db.updateUser(userId, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
import { Turnkey, 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 { getConfig } from './utils';
|
|
||||||
import { Service } from './service';
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,6 @@ export interface GitPushEventPayload {
|
|||||||
id: string;
|
id: string;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
deleted: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppDeploymentRecordAttributes {
|
export interface AppDeploymentRecordAttributes {
|
||||||
@@ -38,13 +37,6 @@ export interface AppDeploymentRecordAttributes {
|
|||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppDeploymentRemovalRecordAttributes {
|
|
||||||
deployment: string;
|
|
||||||
request: string;
|
|
||||||
type: "ApplicationDeploymentRemovalRecord";
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RegistryRecord {
|
interface RegistryRecord {
|
||||||
id: string;
|
id: string;
|
||||||
names: string[] | null;
|
names: string[] | null;
|
||||||
@@ -57,7 +49,3 @@ interface RegistryRecord {
|
|||||||
export interface AppDeploymentRecord extends RegistryRecord {
|
export interface AppDeploymentRecord extends RegistryRecord {
|
||||||
attributes: AppDeploymentRecordAttributes;
|
attributes: AppDeploymentRecordAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppDeploymentRemovalRecord extends RegistryRecord {
|
|
||||||
attributes: AppDeploymentRemovalRecordAttributes;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,18 +3,11 @@ import path from 'path';
|
|||||||
import toml from 'toml';
|
import toml from 'toml';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||||
import { Config } from './config';
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
|
||||||
|
|
||||||
const log = debug('snowball:utils');
|
const log = debug('snowball:utils');
|
||||||
|
|
||||||
export async function getConfig() {
|
export const getConfig = async <ConfigType>(
|
||||||
// TODO: get config path using cli
|
configFile: string
|
||||||
return await _getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
const _getConfig = async <ConfigType>(
|
|
||||||
configFile: string,
|
|
||||||
): Promise<ConfigType> => {
|
): Promise<ConfigType> => {
|
||||||
const configFilePath = path.resolve(configFile);
|
const configFilePath = path.resolve(configFile);
|
||||||
const fileExists = await fs.pathExists(configFilePath);
|
const fileExists = await fs.pathExists(configFilePath);
|
||||||
@@ -48,7 +41,7 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
|||||||
entityType: EntityTarget<Entity>,
|
entityType: EntityTarget<Entity>,
|
||||||
dataSource: DataSource,
|
dataSource: DataSource,
|
||||||
entities: any,
|
entities: any,
|
||||||
relations?: any | undefined,
|
relations?: any | undefined
|
||||||
): Promise<Entity[]> => {
|
): Promise<Entity[]> => {
|
||||||
const entityRepository = dataSource.getRepository(entityType);
|
const entityRepository = dataSource.getRepository(entityType);
|
||||||
|
|
||||||
@@ -63,7 +56,7 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
|||||||
|
|
||||||
entity = {
|
entity = {
|
||||||
...entity,
|
...entity,
|
||||||
[field]: relations[field][entityData[valueIndex]],
|
[field]: relations[field][entityData[valueIndex]]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,6 +66,3 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
|||||||
|
|
||||||
return savedEntity;
|
return savedEntity;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sleep = async (ms: number): Promise<void> =>
|
|
||||||
new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
|
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
|
||||||
"name": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
"name": "Snowball",
|
||||||
"email": "snowball@snowballtools.xyz",
|
"email": "snowball@snowballtools.xyz",
|
||||||
"isVerified": true,
|
"isVerified": true
|
||||||
"ethAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e505b212-8da6-48b2-9614-098225dab34b",
|
"id": "e505b212-8da6-48b2-9614-098225dab34b",
|
||||||
"name": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
|
"name": "Alice Anderson",
|
||||||
"email": "alice@snowballtools.xyz",
|
"email": "alice@snowballtools.xyz",
|
||||||
"isVerified": true,
|
"isVerified": true
|
||||||
"ethAddress": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
|
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
|
||||||
"name": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a",
|
"name": "Bob Banner",
|
||||||
"email": "bob@snowballtools.xyz",
|
"email": "bob@snowballtools.xyz",
|
||||||
"isVerified": true,
|
"isVerified": true
|
||||||
"ethAddress": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
||||||
import { Registry } from '@snowballtools/laconic-sdk';
|
import { Registry } from '@cerc-io/laconic-sdk';
|
||||||
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||||
import { Config } from '../src/config';
|
import { Config } from '../src/config';
|
||||||
@@ -11,13 +11,17 @@ const log = debug('snowball:initialize-registry');
|
|||||||
const DENOM = 'aphoton';
|
const DENOM = 'aphoton';
|
||||||
const BOND_AMOUNT = '1000000000';
|
const BOND_AMOUNT = '1000000000';
|
||||||
|
|
||||||
|
// TODO: Get authority names from args
|
||||||
|
const AUTHORITY_NAMES = ['snowballtools', 'cerc-io'];
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
|
|
||||||
// TODO: Get authority names from args
|
const registry = new Registry(
|
||||||
const authorityNames = ['snowballtools', registryConfig.authority];
|
registryConfig.gqlEndpoint,
|
||||||
|
registryConfig.restEndpoint,
|
||||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
registryConfig.chainId
|
||||||
|
);
|
||||||
|
|
||||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||||
log('bondId:', bondId);
|
log('bondId:', bondId);
|
||||||
@@ -27,8 +31,12 @@ async function main () {
|
|||||||
registryConfig.fee
|
registryConfig.fee
|
||||||
);
|
);
|
||||||
|
|
||||||
for await (const name of authorityNames) {
|
for await (const name of AUTHORITY_NAMES) {
|
||||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
await registry.reserveAuthority(
|
||||||
|
{ name },
|
||||||
|
registryConfig.privateKey,
|
||||||
|
registryConfig.fee
|
||||||
|
);
|
||||||
log('Reserved authority name:', name);
|
log('Reserved authority name:', name);
|
||||||
await registry.setAuthorityBond(
|
await registry.setAuthorityBond(
|
||||||
{ name, bondId },
|
{ name, bondId },
|
||||||
|
|||||||
@@ -2,17 +2,19 @@ import debug from 'debug';
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { Registry } from '@snowballtools/laconic-sdk';
|
import { Registry } from '@cerc-io/laconic-sdk';
|
||||||
|
|
||||||
import { Config } from '../src/config';
|
import { Config } from '../src/config';
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
import { DEFAULT_CONFIG_FILE_PATH, PROJECT_DOMAIN } from '../src/constants';
|
||||||
import { getConfig } from '../src/utils';
|
import { getConfig } from '../src/utils';
|
||||||
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
|
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
||||||
|
|
||||||
const log = debug('snowball:publish-deploy-records');
|
const log = debug('snowball:publish-deploy-records');
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { registryConfig, database } = await getConfig<Config>(
|
||||||
|
DEFAULT_CONFIG_FILE_PATH
|
||||||
|
);
|
||||||
|
|
||||||
const registry = new Registry(
|
const registry = new Registry(
|
||||||
registryConfig.gqlEndpoint,
|
registryConfig.gqlEndpoint,
|
||||||
@@ -40,7 +42,7 @@ async function main () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for await (const deployment of deployments) {
|
for await (const deployment of deployments) {
|
||||||
const url = `https://${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
|
const url = `${deployment.project.name}-${deployment.id}.${PROJECT_DOMAIN}`;
|
||||||
|
|
||||||
const applicationDeploymentRecord = {
|
const applicationDeploymentRecord = {
|
||||||
type: 'ApplicationDeploymentRecord',
|
type: 'ApplicationDeploymentRecord',
|
||||||
@@ -71,21 +73,6 @@ async function main () {
|
|||||||
registryConfig.fee
|
registryConfig.fee
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove deployment for project subdomain if deployment is for production environment
|
|
||||||
if (deployment.environment === Environment.Production) {
|
|
||||||
applicationDeploymentRecord.url = `https://${deployment.project.subDomain}`
|
|
||||||
|
|
||||||
await registry.setRecord(
|
|
||||||
{
|
|
||||||
privateKey: registryConfig.privateKey,
|
|
||||||
record: applicationDeploymentRecord,
|
|
||||||
bondId: registryConfig.bondId
|
|
||||||
},
|
|
||||||
'',
|
|
||||||
registryConfig.fee
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
log('Application deployment record data:', applicationDeploymentRecord);
|
log('Application deployment record data:', applicationDeploymentRecord);
|
||||||
log(`Application deployment record published: ${result.data.id}`);
|
log(`Application deployment record published: ${result.data.id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import debug from 'debug';
|
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import { Registry } from '@cerc-io/laconic-sdk';
|
|
||||||
|
|
||||||
import { Config } from '../src/config';
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
|
||||||
import { getConfig } from '../src/utils';
|
|
||||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
|
||||||
|
|
||||||
const log = debug('snowball:publish-deployment-removal-records');
|
|
||||||
|
|
||||||
async function main () {
|
|
||||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
|
||||||
|
|
||||||
const registry = new Registry(
|
|
||||||
registryConfig.gqlEndpoint,
|
|
||||||
registryConfig.restEndpoint,
|
|
||||||
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 result = await registry.setRecord(
|
|
||||||
{
|
|
||||||
privateKey: registryConfig.privateKey,
|
|
||||||
record: applicationDeploymentRemovalRecord,
|
|
||||||
bondId: registryConfig.bondId
|
|
||||||
},
|
|
||||||
'',
|
|
||||||
registryConfig.fee
|
|
||||||
);
|
|
||||||
|
|
||||||
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
|
|
||||||
log(`Application deployment removal record published: ${result.data.id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
log(err);
|
|
||||||
});
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# deployer
|
|
||||||
|
|
||||||
- Install dependencies
|
|
||||||
```bash
|
|
||||||
yarn
|
|
||||||
```
|
|
||||||
```bash
|
|
||||||
brew install jq # if you do not have jq installed already
|
|
||||||
```
|
|
||||||
|
|
||||||
- Run script to deploy app
|
|
||||||
|
|
||||||
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
|
|
||||||
|
|
||||||
```
|
|
||||||
./deploy-frontend.staging.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
- To deploy frontend app to `dashboard.apps.snowballtools.com`
|
|
||||||
|
|
||||||
```
|
|
||||||
./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
|
|
||||||
# Account balance
|
|
||||||
yarn laconic cns account get
|
|
||||||
|
|
||||||
# Bond balance
|
|
||||||
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
|
||||||
```
|
|
||||||
- Command to refill bond
|
|
||||||
```bash
|
|
||||||
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
|
|
||||||
```
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
services:
|
|
||||||
cns:
|
|
||||||
restEndpoint: 'http://console.laconic.com:1317'
|
|
||||||
gqlEndpoint: 'http://console.laconic.com:9473/api'
|
|
||||||
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
|
|
||||||
bondId: 098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661
|
|
||||||
chainId: laconic_9000-1
|
|
||||||
gas: 550000
|
|
||||||
fees: 200000aphoton
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
services:
|
|
||||||
cns:
|
|
||||||
restEndpoint: http://console.laconic.com:1317
|
|
||||||
gqlEndpoint: http://console.laconic.com:9473/api
|
|
||||||
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
|
|
||||||
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
|
||||||
chainId: laconic_9000-1
|
|
||||||
gas: 1200000
|
|
||||||
fees: 200000aphoton
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Repository URL
|
|
||||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
|
||||||
|
|
||||||
# Get the latest commit hash from the repository
|
|
||||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
|
||||||
|
|
||||||
# Extract version from ../frontend/package.json
|
|
||||||
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
|
||||||
|
|
||||||
# Current date and time for note
|
|
||||||
CURRENT_DATE_TIME=$(date -u)
|
|
||||||
|
|
||||||
CONFIG_FILE=config.yml
|
|
||||||
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
|
|
||||||
|
|
||||||
# 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=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "snowballtools-base-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
|
|
||||||
NEW_APPLICATION_VERSION=0.0.1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Generate application-deployment-request.yml
|
|
||||||
cat > ./records/application-deployment-request.yml <<EOF
|
|
||||||
record:
|
|
||||||
type: ApplicationDeploymentRequest
|
|
||||||
version: '1.0.0'
|
|
||||||
name: snowballtools-base-frontend@$PACKAGE_VERSION
|
|
||||||
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
|
|
||||||
dns: dashboard
|
|
||||||
config:
|
|
||||||
env:
|
|
||||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
|
||||||
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
|
|
||||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
|
||||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
|
||||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
|
||||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
|
||||||
meta:
|
|
||||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
|
||||||
repository: "$REPO_URL"
|
|
||||||
repository_ref: $LATEST_HASH
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Generate application-record.yml with incremented version
|
|
||||||
cat > ./records/application-record.yml <<EOF
|
|
||||||
record:
|
|
||||||
type: ApplicationRecord
|
|
||||||
version: $NEW_APPLICATION_VERSION
|
|
||||||
repository_ref: $LATEST_HASH
|
|
||||||
repository: ["$REPO_URL"]
|
|
||||||
app_type: webapp
|
|
||||||
name: snowballtools-base-frontend
|
|
||||||
app_version: $PACKAGE_VERSION
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Files generated successfully."
|
|
||||||
|
|
||||||
RECORD_FILE=records/application-record.yml
|
|
||||||
|
|
||||||
# Publish ApplicationRecord
|
|
||||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
|
||||||
echo "ApplicationRecord published"
|
|
||||||
echo $RECORD_ID
|
|
||||||
|
|
||||||
# Set name to record
|
|
||||||
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
|
||||||
sleep 2
|
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
|
||||||
sleep 2
|
|
||||||
# Set name if latest release
|
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
|
||||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
|
||||||
|
|
||||||
# Check if record found for REGISTRY_APP_CRN
|
|
||||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
|
||||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
|
||||||
echo "No record found for $REGISTRY_APP_CRN."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
RECORD_FILE=records/application-deployment-request.yml
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
|
||||||
echo "ApplicationDeploymentRequest published"
|
|
||||||
echo $DEPLOYMENT_REQUEST_ID
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Repository URL
|
|
||||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
|
||||||
|
|
||||||
# Get the latest commit hash from the repository
|
|
||||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
|
||||||
|
|
||||||
# Extract version from ../frontend/package.json
|
|
||||||
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
|
||||||
|
|
||||||
# Current date and time for note
|
|
||||||
CURRENT_DATE_TIME=$(date -u)
|
|
||||||
|
|
||||||
CONFIG_FILE=config.staging.yml
|
|
||||||
REGISTRY_BOND_ID="098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661"
|
|
||||||
|
|
||||||
# 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=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "staging-snowballtools-base-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
|
|
||||||
NEW_APPLICATION_VERSION=0.0.1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Generate application-deployment-request.yml
|
|
||||||
cat > ./staging-records/application-deployment-request.yml <<EOF
|
|
||||||
record:
|
|
||||||
type: ApplicationDeploymentRequest
|
|
||||||
version: '1.0.0'
|
|
||||||
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
|
||||||
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
|
||||||
dns: dashboard.staging.apps.snowballtools.com
|
|
||||||
config:
|
|
||||||
env:
|
|
||||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
|
|
||||||
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
|
|
||||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
|
|
||||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
|
|
||||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
|
||||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
|
||||||
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
|
||||||
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
|
||||||
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
|
||||||
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
|
|
||||||
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
|
|
||||||
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
|
||||||
meta:
|
|
||||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
|
||||||
repository: "$REPO_URL"
|
|
||||||
repository_ref: $LATEST_HASH
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Generate application-record.yml with incremented version
|
|
||||||
cat > ./staging-records/application-record.yml <<EOF
|
|
||||||
record:
|
|
||||||
type: ApplicationRecord
|
|
||||||
version: $NEW_APPLICATION_VERSION
|
|
||||||
repository_ref: $LATEST_HASH
|
|
||||||
repository: ["$REPO_URL"]
|
|
||||||
app_type: webapp
|
|
||||||
name: staging-snowballtools-base-frontend
|
|
||||||
app_version: $PACKAGE_VERSION
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Files generated successfully."
|
|
||||||
|
|
||||||
RECORD_FILE=staging-records/application-record.yml
|
|
||||||
|
|
||||||
# Publish ApplicationRecord
|
|
||||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
|
||||||
echo "ApplicationRecord published"
|
|
||||||
echo $RECORD_ID
|
|
||||||
|
|
||||||
# Set name to record
|
|
||||||
REGISTRY_APP_CRN="crn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
|
||||||
sleep 2
|
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
|
||||||
sleep 2
|
|
||||||
# Set name if latest release
|
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
|
||||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
|
||||||
|
|
||||||
# Check if record found for REGISTRY_APP_CRN
|
|
||||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
|
||||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
|
||||||
echo "No record found for $REGISTRY_APP_CRN."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
RECORD_FILE=staging-records/application-deployment-request.yml
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
|
||||||
echo "ApplicationDeploymentRequest published"
|
|
||||||
echo $DEPLOYMENT_REQUEST_ID
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "deployer",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"private": true,
|
|
||||||
"devDependencies": {
|
|
||||||
"@snowballtools/laconic-registry-cli": "^0.1.13"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
record:
|
|
||||||
type: ApplicationDeploymentRequest
|
|
||||||
version: '1.0.0'
|
|
||||||
name: snowballtools-base-frontend@0.1.8
|
|
||||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
|
|
||||||
dns: dashboard
|
|
||||||
config:
|
|
||||||
env:
|
|
||||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
|
||||||
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
|
||||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
|
||||||
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
|
||||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
|
||||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
|
||||||
meta:
|
|
||||||
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
|
||||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
|
||||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
record:
|
|
||||||
type: ApplicationRecord
|
|
||||||
version: 0.0.2
|
|
||||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
|
||||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
|
||||||
app_type: webapp
|
|
||||||
name: snowballtools-base-frontend
|
|
||||||
app_version: 0.1.8
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
record:
|
|
||||||
type: ApplicationDeploymentRequest
|
|
||||||
version: '1.0.0'
|
|
||||||
name: staging-snowballtools-base-frontend@0.0.0
|
|
||||||
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@0.0.0
|
|
||||||
dns: dashboard.staging.apps.snowballtools.com
|
|
||||||
config:
|
|
||||||
env:
|
|
||||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
|
|
||||||
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
|
|
||||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
|
|
||||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
|
|
||||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
|
||||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
|
||||||
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
|
||||||
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
|
||||||
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
|
||||||
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
|
|
||||||
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
|
|
||||||
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
|
||||||
meta:
|
|
||||||
note: Added by Snowball @ Tuesday 21 May 2024 06:17:23 AM UTC
|
|
||||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
|
||||||
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
record:
|
|
||||||
type: ApplicationRecord
|
|
||||||
version: 0.0.2
|
|
||||||
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
|
|
||||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
|
||||||
app_type: webapp
|
|
||||||
name: staging-snowballtools-base-frontend
|
|
||||||
app_version: 0.0.0
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# deployer test
|
|
||||||
|
|
||||||
Check if the live web app deployer is in a working state
|
|
||||||
|
|
||||||
- Web app repo used: https://github.com/snowball-tools-platform/test-progressive-web-app (main branch)
|
|
||||||
- Config used: [../config.yml](../config.yml)
|
|
||||||
- The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following:
|
|
||||||
- Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo
|
|
||||||
- Fetch the latest version of `deployment-test-app` from registry and increment `ApplicationRecord` version
|
|
||||||
- Publish the resulting `ApplicationRecord` record
|
|
||||||
- Set names to the record and check name resolution
|
|
||||||
- Publish the `ApplicationDeploymentRequest` record
|
|
||||||
- Check that the deployment occurs
|
|
||||||
- Check that a `ApplicationDeploymentRecord` is created
|
|
||||||
- Check that the deployment record has correct `ApplicationRecord` id
|
|
||||||
- Check that the URL present in deployment record is active
|
|
||||||
- Create and publish a [`ApplicationDeploymentRemovalRequest`](./records/application-deployment-removal-request.yml) record
|
|
||||||
- Check that the deployment is removed
|
|
||||||
- Check that a `ApplicationDeploymentRemovalRecord` is created
|
|
||||||
- Check that the deployment URL goes down
|
|
||||||
- The test script is run in a GitHub CI [workflow](../../../.github/workflows/test-app-deployment.yaml) that:
|
|
||||||
- Is scheduled to run everyday on the default (`main`) branch or can be triggered manually
|
|
||||||
- Sends Slack alerts to configured channels on failure
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
record:
|
|
||||||
deployment: <APPLICATION_DEPLOYMENT_RECORD_ID>
|
|
||||||
type: ApplicationDeploymentRemovalRequest
|
|
||||||
version: 1.0.0
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
record:
|
|
||||||
type: ApplicationDeploymentRequest
|
|
||||||
version: '1.0.0'
|
|
||||||
name: deployment-test-app@0.1.24
|
|
||||||
application: crn://snowballtools/applications/deployment-test-app@0.1.24
|
|
||||||
dns: deployment-ci-test
|
|
||||||
config:
|
|
||||||
env:
|
|
||||||
CERC_TEST_WEBAPP_CONFIG1: "deployment test config 1"
|
|
||||||
CERC_TEST_WEBAPP_CONFIG2: "deployment test config 2"
|
|
||||||
CERC_WEBAPP_DEBUG: 0
|
|
||||||
meta:
|
|
||||||
note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC
|
|
||||||
repository: "https://github.com/snowball-tools-platform/test-progressive-web-app"
|
|
||||||
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
record:
|
|
||||||
type: ApplicationRecord
|
|
||||||
version: 0.0.1
|
|
||||||
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
|
||||||
repository: ["https://github.com/snowball-tools-platform/test-progressive-web-app"]
|
|
||||||
app_type: webapp
|
|
||||||
name: deployment-test-app
|
|
||||||
app_version: 0.1.24
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Repository URL
|
|
||||||
REPO_URL="https://github.com/snowball-tools-platform/test-progressive-web-app"
|
|
||||||
|
|
||||||
# Get the latest commit hash from the repository
|
|
||||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
|
||||||
|
|
||||||
# Fetch the package.json file content
|
|
||||||
# Extract version from package.json content
|
|
||||||
package_json=$(wget -qO- "$REPO_URL/raw/$LATEST_HASH/package.json")
|
|
||||||
PACKAGE_VERSION=$(echo "$package_json" | jq -r '.version')
|
|
||||||
|
|
||||||
# Current date and time for note
|
|
||||||
CURRENT_DATE_TIME=$(date -u)
|
|
||||||
|
|
||||||
CONFIG_FILE=packages/deployer/config.yml
|
|
||||||
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
|
|
||||||
|
|
||||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
|
||||||
|
|
||||||
APP_NAME=deployment-test-app
|
|
||||||
|
|
||||||
# Get latest version from registry and increment application-record version
|
|
||||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "$APP_NAME" 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
|
|
||||||
NEW_APPLICATION_VERSION=0.0.1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Generate application-record.yml with incremented version
|
|
||||||
RECORD_FILE=packages/deployer/test/records/application-record.yml
|
|
||||||
|
|
||||||
cat > $RECORD_FILE <<EOF
|
|
||||||
record:
|
|
||||||
type: ApplicationRecord
|
|
||||||
version: $NEW_APPLICATION_VERSION
|
|
||||||
repository_ref: $LATEST_HASH
|
|
||||||
repository: ["$REPO_URL"]
|
|
||||||
app_type: webapp
|
|
||||||
name: $APP_NAME
|
|
||||||
app_version: $PACKAGE_VERSION
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Generate application-deployment-request.yml
|
|
||||||
REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-request.yml
|
|
||||||
|
|
||||||
cat > $REQUEST_RECORD_FILE <<EOF
|
|
||||||
record:
|
|
||||||
type: ApplicationDeploymentRequest
|
|
||||||
version: '1.0.0'
|
|
||||||
name: $APP_NAME@$PACKAGE_VERSION
|
|
||||||
application: crn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
|
|
||||||
dns: deployment-ci-test
|
|
||||||
config:
|
|
||||||
env:
|
|
||||||
CERC_TEST_WEBAPP_CONFIG1: "deployment test config 1"
|
|
||||||
CERC_TEST_WEBAPP_CONFIG2: "deployment test config 2"
|
|
||||||
CERC_WEBAPP_DEBUG: 0
|
|
||||||
meta:
|
|
||||||
note: Deployment test @ $CURRENT_DATE_TIME
|
|
||||||
repository: "$REPO_URL"
|
|
||||||
repository_ref: $LATEST_HASH
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Record files generated successfully."
|
|
||||||
|
|
||||||
# Publish ApplicationRecord
|
|
||||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
|
||||||
echo "ApplicationRecord published"
|
|
||||||
echo $RECORD_ID
|
|
||||||
|
|
||||||
# Set name to record
|
|
||||||
REGISTRY_APP_CRN="crn://snowballtools/applications/$APP_NAME"
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
|
||||||
sleep 2
|
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
|
||||||
sleep 2
|
|
||||||
# Set name if latest release
|
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
|
||||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
|
||||||
|
|
||||||
# Check if record exists for REGISTRY_APP_CRN
|
|
||||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
|
||||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
|
||||||
echo "No record found for $REGISTRY_APP_CRN."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
|
|
||||||
echo "ApplicationDeploymentRequest published"
|
|
||||||
echo $DEPLOYMENT_REQUEST_ID
|
|
||||||
|
|
||||||
# Deployment checks
|
|
||||||
RETRY_INTERVAL=30
|
|
||||||
MAX_RETRIES=20
|
|
||||||
|
|
||||||
# Check that a ApplicationDeploymentRecord is published
|
|
||||||
retry_count=0
|
|
||||||
while true; do
|
|
||||||
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
|
|
||||||
len_deployment_records=$(echo $deployment_records_response | jq 'length')
|
|
||||||
|
|
||||||
# Check if number of records returned is 0
|
|
||||||
if [ $len_deployment_records -eq 0 ]; then
|
|
||||||
# Check if retries are exhausted
|
|
||||||
if [ $retry_count -eq $MAX_RETRIES ]; then
|
|
||||||
echo "Retries exhausted"
|
|
||||||
echo "ApplicationDeploymentRecord for deployment request $DEPLOYMENT_REQUEST_ID not found, exiting"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "ApplicationDeploymentRecord not found, retrying in $RETRY_INTERVAL sec..."
|
|
||||||
sleep $RETRY_INTERVAL
|
|
||||||
retry_count=$((retry_count+1))
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "ApplicationDeploymentRecord found"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
DEPLOYMENT_RECORD_ID=$(echo $deployment_records_response | jq -r '.[0].id')
|
|
||||||
echo $DEPLOYMENT_RECORD_ID
|
|
||||||
|
|
||||||
# Check if ApplicationDeploymentRecord has the correct record id
|
|
||||||
fetched_application_record_id=$(echo $deployment_records_response | jq -r '.[0].attributes.application')
|
|
||||||
if [ "$fetched_application_record_id" = "$RECORD_ID" ]; then
|
|
||||||
echo "ApplicationRecord id matched"
|
|
||||||
else
|
|
||||||
echo "ApplicationRecord id does not match, expected: $RECORD_ID, received: $fetched_application_record_id"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the url present in ApplicationDeploymentRecord is active
|
|
||||||
fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
|
|
||||||
|
|
||||||
retry_count=0
|
|
||||||
max_retries=10
|
|
||||||
retry_interval=5
|
|
||||||
while true; do
|
|
||||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
|
||||||
if [ "$url_response" = "200" ]; then
|
|
||||||
echo "Deployment URL $fetched_url is active"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
if [ $retry_count -eq $max_retries ]; then
|
|
||||||
echo "Retries exhausted"
|
|
||||||
echo "Deployment URL $fetched_url is not active, exiting"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "Deployment URL $fetched_url is not active, received code $url_response, retrying in $retry_interval sec..."
|
|
||||||
sleep $retry_interval
|
|
||||||
retry_count=$((retry_count+1))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Generate application-deployment-removal-request.yml
|
|
||||||
REMOVAL_REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-removal-request.yml
|
|
||||||
|
|
||||||
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
|
|
||||||
record:
|
|
||||||
deployment: $DEPLOYMENT_RECORD_ID
|
|
||||||
type: ApplicationDeploymentRemovalRequest
|
|
||||||
version: 1.0.0
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
|
||||||
echo "ApplicationDeploymentRemovalRequest published"
|
|
||||||
echo $REMOVAL_REQUEST_ID
|
|
||||||
|
|
||||||
# Check that an ApplicationDeploymentRemovalRecord is published
|
|
||||||
retry_count=0
|
|
||||||
while true; do
|
|
||||||
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
|
||||||
len_removal_records=$(echo $removal_records_response | jq 'length')
|
|
||||||
|
|
||||||
# Check if number of records returned is 0
|
|
||||||
if [ $len_removal_records -eq 0 ]; then
|
|
||||||
# Check if retries are exhausted
|
|
||||||
if [ $retry_count -eq $MAX_RETRIES ]; then
|
|
||||||
echo "Retries exhausted"
|
|
||||||
echo "ApplicationDeploymentRemovalRecord for deployment removal request $REMOVAL_REQUEST_ID not found"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
|
|
||||||
sleep $RETRY_INTERVAL
|
|
||||||
retry_count=$((retry_count+1))
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "ApplicationDeploymentRemovalRecord found"
|
|
||||||
REMOVAL_RECORD_ID=$(echo $removal_records_response | jq -r '.[0].id')
|
|
||||||
echo $REMOVAL_RECORD_ID
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check if the application url is down after deployment removal
|
|
||||||
retry_count=0
|
|
||||||
max_retries=10
|
|
||||||
retry_interval=5
|
|
||||||
while true; do
|
|
||||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
|
||||||
if [ "$url_response" = "404" ]; then
|
|
||||||
echo "Deployment URL $fetched_url is down"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
if [ $retry_count -eq $max_retries ]; then
|
|
||||||
echo "Retries exhausted"
|
|
||||||
echo "Deployment URL $fetched_url is still active, exiting"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "Deployment URL $fetched_url is still active, received code $url_response, retrying in $retry_interval sec..."
|
|
||||||
sleep $retry_interval
|
|
||||||
retry_count=$((retry_count+1))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Test successful"
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
|
||||||
|
|
||||||
|
REACT_APP_GITHUB_CLIENT_ID =
|
||||||
|
REACT_APP_GITHUB_TEMPLATE_REPO =
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
VITE_SERVER_URL='http://localhost:8000'
|
|
||||||
|
|
||||||
VITE_GITHUB_CLIENT_ID=
|
|
||||||
VITE_GITHUB_PWA_TEMPLATE_REPO="snowball-tools/test-progressive-web-app"
|
|
||||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO="snowball-tools/image-upload-pwa-example"
|
|
||||||
|
|
||||||
VITE_WALLET_CONNECT_ID=
|
|
||||||
|
|
||||||
VITE_LIT_RELAY_API_KEY=
|
|
||||||
|
|
||||||
VITE_ALCHEMY_API_KEY=
|
|
||||||
|
|
||||||
VITE_BUGSNAG_API_KEY=
|
|
||||||
|
|
||||||
VITE_PASSKEY_WALLET_RPID=
|
|
||||||
VITE_TURNKEY_API_BASE_URL=
|
|
||||||
VITE_TURNKEY_ORGANIZATION_ID=
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
build
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: { browser: true, es2020: true },
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
'plugin:storybook/recommended',
|
|
||||||
],
|
|
||||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['react-refresh'],
|
|
||||||
rules: {
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": 13,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"plugins": ["react", "@typescript-eslint"],
|
||||||
|
"extends": [
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
@@ -22,4 +21,3 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
*storybook.log
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
v20.12.1
|
|
||||||
@@ -1 +1,3 @@
|
|||||||
dist/
|
# artifacts
|
||||||
|
build
|
||||||
|
coverage
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import type { StorybookConfig } from '@storybook/react-vite';
|
|
||||||
|
|
||||||
import { join, dirname } from 'path';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is used to resolve the absolute path of a package.
|
|
||||||
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
|
||||||
*/
|
|
||||||
function getAbsolutePath(value: string): any {
|
|
||||||
return dirname(require.resolve(join(value, 'package.json')));
|
|
||||||
}
|
|
||||||
const config: StorybookConfig = {
|
|
||||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
|
||||||
addons: [
|
|
||||||
getAbsolutePath('@storybook/addon-onboarding'),
|
|
||||||
getAbsolutePath('@storybook/addon-links'),
|
|
||||||
getAbsolutePath('@storybook/addon-essentials'),
|
|
||||||
getAbsolutePath('@chromatic-com/storybook'),
|
|
||||||
getAbsolutePath('@storybook/addon-interactions'),
|
|
||||||
getAbsolutePath('storybook-addon-remix-react-router'),
|
|
||||||
],
|
|
||||||
framework: {
|
|
||||||
name: getAbsolutePath('@storybook/react-vite'),
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
docs: {
|
|
||||||
autodocs: 'tag',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import type { Preview } from '@storybook/react';
|
|
||||||
|
|
||||||
import '../src/index.css';
|
|
||||||
|
|
||||||
const preview: Preview = {
|
|
||||||
parameters: {
|
|
||||||
controls: {
|
|
||||||
matchers: {
|
|
||||||
color: /(background|color)$/i,
|
|
||||||
date: /Date$/i,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default preview;
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
// eslint extension options
|
||||||
|
"eslint.enable": true,
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact"
|
||||||
|
],
|
||||||
|
"css.customData": [".vscode/tailwind.json"],
|
||||||
|
// prettier extension setting
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"editor.rulers": [80],
|
||||||
|
"editor.codeActionsOnSave": [
|
||||||
|
"source.addMissingImports",
|
||||||
|
"source.fixAll",
|
||||||
|
"source.organizeImports"
|
||||||
|
],
|
||||||
|
// Show in vscode "Problems" tab when there are errors
|
||||||
|
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
||||||
|
// Use absolute import for typescript files
|
||||||
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
|
// IntelliSense for taiwind variants
|
||||||
|
"tailwindCSS.experimental.classRegex": [
|
||||||
|
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,30 +1,46 @@
|
|||||||
# React + TypeScript + Vite
|
# Getting Started with Create React App
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using [typescript-tailwindcss-eslint-prettier](https://github.com/cufarvid/cra-templates) template.
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
## Available Scripts
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
In the project directory, you can run:
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
### `yarn start`
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
- Configure the top-level `parserOptions` property like this:
|
The page will reload if you make edits.\
|
||||||
|
You will also see any lint errors in the console.
|
||||||
|
|
||||||
```js
|
### `yarn test`
|
||||||
export default {
|
|
||||||
// other rules...
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
Launches the test runner in the interactive watch mode.\
|
||||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
|
||||||
|
### `yarn build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `yarn eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||||
|
|
||||||
|
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||||
|
|
||||||
|
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"projectId": "Project:663d04870db27ed66a48e466",
|
|
||||||
"zip": true
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="description" content="snowball tools dashboard" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<meta name="msapplication-TileColor" content="#2d89ef" />
|
|
||||||
<meta name="theme-color" content="#ffffff" />
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
|
||||||
<title>Snowball</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,58 +1,29 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite --port 3000",
|
|
||||||
"build": "vite build",
|
|
||||||
"lint": "tsc --noEmit",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"format": "prettier --write .",
|
|
||||||
"storybook": "storybook dev -p 6006",
|
|
||||||
"build-storybook": "storybook build"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bugsnag/browser-performance": "^2.4.1",
|
|
||||||
"@bugsnag/js": "^7.22.7",
|
|
||||||
"@bugsnag/plugin-react": "^7.22.7",
|
|
||||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
|
||||||
"@fontsource/inter": "^5.0.16",
|
"@fontsource/inter": "^5.0.16",
|
||||||
|
"@material-tailwind/react": "^2.1.7",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@snowballtools/auth": "^0.2.0",
|
|
||||||
"@snowballtools/auth-lit": "^0.2.0",
|
|
||||||
"@snowballtools/js-sdk": "^0.1.1",
|
|
||||||
"@snowballtools/link-lit-alchemy-light": "^0.2.0",
|
|
||||||
"@snowballtools/material-tailwind-react-fork": "^2.1.10",
|
|
||||||
"@snowballtools/smartwallet-alchemy-light": "^0.2.0",
|
|
||||||
"@snowballtools/types": "^0.2.0",
|
|
||||||
"@snowballtools/utils": "^0.1.1",
|
|
||||||
"@tanstack/react-query": "^5.22.2",
|
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@turnkey/http": "^2.10.0",
|
"@types/jest": "^27.5.2",
|
||||||
"@turnkey/sdk-react": "^0.1.0",
|
"@types/node": "^16.18.68",
|
||||||
"@turnkey/webauthn-stamper": "^0.5.0",
|
"@types/react": "^18.2.42",
|
||||||
"@walletconnect/ethereum-provider": "^2.12.2",
|
"@types/react-dom": "^18.2.17",
|
||||||
"@web3modal/siwe": "^4.0.5",
|
|
||||||
"@web3modal/wagmi": "^4.0.5",
|
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
"axios": "^1.6.7",
|
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
"downshift": "^8.3.2",
|
"downshift": "^8.2.3",
|
||||||
"framer-motion": "^11.0.8",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"gql-client": "^1.0.0",
|
"gql-client": "^1.0.0",
|
||||||
"lottie-react": "^2.4.0",
|
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"octokit": "^3.1.2",
|
"octokit": "^3.1.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -65,41 +36,51 @@
|
|||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-oauth-popup": "^1.0.5",
|
"react-oauth-popup": "^1.0.5",
|
||||||
"react-router-dom": "^6.20.1",
|
"react-router-dom": "^6.20.1",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
"react-timer-hook": "^3.0.7",
|
"react-timer-hook": "^3.0.7",
|
||||||
"siwe": "^2.1.4",
|
|
||||||
"tailwind-variants": "^0.2.0",
|
"tailwind-variants": "^0.2.0",
|
||||||
"usehooks-ts": "^2.15.1",
|
"typescript": "^4.9.5",
|
||||||
"uuid": "^9.0.1",
|
"usehooks-ts": "^2.10.0",
|
||||||
"viem": "^2.7.11",
|
"vertical-stepper-nav": "^1.0.2",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"lint": "eslint ."
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "^1.3.3",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@storybook/addon-essentials": "^8.0.10",
|
|
||||||
"@storybook/addon-interactions": "^8.0.10",
|
|
||||||
"@storybook/addon-links": "^8.0.10",
|
|
||||||
"@storybook/addon-onboarding": "^8.0.10",
|
|
||||||
"@storybook/blocks": "^8.0.10",
|
|
||||||
"@storybook/react": "^8.0.10",
|
|
||||||
"@storybook/react-vite": "^8.0.10",
|
|
||||||
"@storybook/test": "^8.0.10",
|
|
||||||
"@types/jest": "^27.5.2",
|
|
||||||
"@types/lodash": "^4.17.0",
|
|
||||||
"@types/luxon": "^3.3.7",
|
"@types/luxon": "^3.3.7",
|
||||||
"@types/node": "^16.18.68",
|
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||||
"@types/react": "^18.2.66",
|
"@typescript-eslint/parser": "^6.13.2",
|
||||||
"@types/react-dom": "^18.2.22",
|
"eslint": "^8.55.0",
|
||||||
"@types/uuid": "^9.0.8",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"chromatic": "^11.3.2",
|
|
||||||
"eslint-plugin-storybook": "^0.8.0",
|
|
||||||
"postcss": "^8.4.38",
|
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.1.0",
|
||||||
"storybook": "^8.0.10",
|
"tailwindcss": "^3.4.1"
|
||||||
"storybook-addon-remix-react-router": "^3.0.0",
|
|
||||||
"tailwindcss": "^3.4.3",
|
|
||||||
"typescript": "^5.3.3",
|
|
||||||
"vite": "^5.2.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/mstile-150x150.png"/>
|
|
||||||
<TileColor>#2d89ef</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<svg width="197" height="2" viewBox="0 0 197 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<line x1="0.5" y1="1.19141" x2="197" y2="1.19141" stroke="#94A7B8" stroke-dasharray="1 12"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 196 B |
|
Before Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 989 B |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 3.8 KiB |
@@ -2,34 +2,42 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="description" content="snowball tools dashboard" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
<link
|
<!--
|
||||||
rel="apple-touch-icon"
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
sizes="180x180"
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
href="%PUBLIC_URL%/apple-touch-icon.png"
|
-->
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="32x32"
|
|
||||||
href="%PUBLIC_URL%/favicon-32x32.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="16x16"
|
|
||||||
href="%PUBLIC_URL%/favicon-16x16.png"
|
|
||||||
/>
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
|
|
||||||
<meta name="msapplication-TileColor" content="#2d89ef" />
|
|
||||||
<meta name="theme-color" content="#ffffff" />
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
<title>Snowball</title>
|
<title>Snowball</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="500" height="500" fill="#0F86F5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/><path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/><path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Snowball Tools Dashboard",
|
|
||||||
"short_name": "snowball tools",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"theme_color": "#ffffff",
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
||||||