Compare commits

..
Author SHA1 Message Date
Vivian Phung efc5a45bac Fix Input component react hook 2024-05-22 19:04:59 +00:00
56 changed files with 746 additions and 1275 deletions
-3
View File
@@ -7,6 +7,3 @@ yarn-error.log
packages/backend/environments/local.toml packages/backend/environments/local.toml
packages/backend/dev/ packages/backend/dev/
packages/frontend/dist/ packages/frontend/dist/
# ignore all .DS_Store files
**/.DS_Store
+207 -10
View File
@@ -1,23 +1,220 @@
# snowballtools-base # snowballtools-base
This is a [yarn workspace](https://yarnpkg.com/features/workspaces) monorepo for the dashboard. ## Setup
## Getting Started - Clone the `snowballtools-base` repo
### Install dependencies ```bash
git clone git@github.com:snowball-tools/snowballtools-base.git
cd snowballtools-base
```
In the root of the project, run: - Install dependencies in root
```zsh ```bash
yarn yarn
``` ```
### Build backend - Build packages
```zsh ```bash
yarn build --ignore-frontend yarn build --ignore frontend
``` ```
### Environment variables, running the development server, and deployment Let us assume the following domains for backend and frontend
- Backend server: `api.snowballtools.com`
- Frontend app: `dashboard.snowballtools.com`
Follow the instructions in the README.md files of the [backend](packages/backend/README.md) and [frontend](packages/frontend/README.md) packages. - Configuration Files
- For Backend:
```bash
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
```
- Production
- 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
laconic-so deployment --dir laconic-loaded-deployment 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
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
```
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
- Run the script to create bond, reserve the authority and set authority bond
```bash
yarn test:registry:init
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
```
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
- Setup ngrok for GitHub webhooks
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
- Start ngrok and point to backend server endpoint
```bash
ngrok http http://localhost:8000
```
- Look for the forwarding URL in ngrok
```bash
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)
```toml
[gitHub]
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
```
- For Frontend:
```bash
cp packages/frontend/.env.example packages/frontend/.env
```
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
```env
VITE_GITHUB_CLIENT_ID = <CLIENT_ID>
```
- Set `VITE_GITHUB_PWA_TEMPLATE_REPO` and `VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
```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
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 the following values in [.env](packages/frontend/.env) file
```env
VITE_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint
```
- 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`
- 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
VITE_WALLET_CONNECT_ID = <PROJECT_ID>
```
- Build the React application
```bash
yarn build
```
- Use a web server for hosting static built files
```bash
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 -76
View File
@@ -1,76 +1 @@
# backend # Backend for Snowball Tools
This backend is a [node.js](https://nodejs.org/) [express.js](https://expressjs.com/) [apollo server](https://www.apollographql.com/docs/apollo-server/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
## Getting Started
### Install dependencies
In the root of the project, run:
```zsh
yarn
```
### Build backend
```zsh
yarn build --ignore-frontend
```
### Environment variables
#### Local
Copy the `envionments/local.toml.example` file to `envionments/local.toml`:
```zsh
cp envionments/local.toml.example envionments/local.toml
```
#### Staging environment variables
In the deployment repository, update staging [staging/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/staging/configmaps/config/prod.toml)
#### Production environment variables
In the deployment repository, update production [production/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/production/configmaps/config/prod.toml)
### Run development server
```zsh
yarn start
```
## Deployment
Clone the deployer repository:
```zsh
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
```
### Staging
```zsh
echo trigger >> .gitea/workflows/triggers/staging-deploy
git commit -a -m "Deploy v0.0.8" # replace with version number
git push
```
### Production
```zsh
echo trigger >> .gitea/workflows/triggers/production-deploy
git commit -a -m "Deploy v0.0.8" # replace with version number
git push
```
### Deployment status
Dumb for now
- [Staging](https://snowballtools-base-api.staging.apps.snowballtools.com/staging/version)
- [Production](https://snowballtools-base-api.apps.snowballtools.com/staging/version)
Update version number manually in [routes/staging.ts](/packages/backend/src/routes/staging.ts)
-1
View File
@@ -19,7 +19,6 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"debug": "^4.3.1", "debug": "^4.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-async-errors": "^3.1.1",
"express-session": "^1.18.0", "express-session": "^1.18.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"graphql": "^16.8.1", "graphql": "^16.8.1",
-1
View File
@@ -1,4 +1,3 @@
import 'express-async-errors';
import 'reflect-metadata'; import 'reflect-metadata';
import debug from 'debug'; import debug from 'debug';
import fs from 'fs'; import fs from 'fs';
+38 -56
View File
@@ -25,15 +25,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectById(projectId); return service.getProjectById(projectId);
}, },
projectsInOrganization: async ( projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
_: any, return service.getProjectsInOrganization(context.user, organizationSlug);
{ organizationSlug }: { organizationSlug: string },
context: any,
) => {
return service.getProjectsInOrganization(
context.user,
organizationSlug,
);
}, },
deployments: async (_: any, { projectId }: { projectId: string }) => { deployments: async (_: any, { projectId }: { projectId: string }) => {
@@ -42,7 +35,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
environmentVariables: async ( environmentVariables: async (
_: any, _: any,
{ projectId }: { projectId: string }, { projectId }: { projectId: string }
) => { ) => {
return service.getEnvironmentVariablesByProjectId(projectId); return service.getEnvironmentVariablesByProjectId(projectId);
}, },
@@ -51,11 +44,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectMembersByProjectId(projectId); return service.getProjectMembersByProjectId(projectId);
}, },
searchProjects: async ( searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
_: any,
{ searchText }: { searchText: string },
context: any,
) => {
return service.searchProjects(context.user, searchText); return service.searchProjects(context.user, searchText);
}, },
@@ -63,11 +52,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any, _: any,
{ {
projectId, projectId,
filter, filter
}: { projectId: string; filter?: FindOptionsWhere<Domain> }, }: { projectId: string; filter?: FindOptionsWhere<Domain> }
) => { ) => {
return service.getDomainsByProjectId(projectId, filter); return service.getDomainsByProjectId(projectId, filter);
}, }
}, },
// TODO: Return error in GQL response // TODO: Return error in GQL response
@@ -75,13 +64,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
removeProjectMember: async ( removeProjectMember: async (
_: any, _: any,
{ projectMemberId }: { projectMemberId: string }, { projectMemberId }: { projectMemberId: string },
context: any, context: any
) => { ) => {
try { try {
return await service.removeProjectMember( return await service.removeProjectMember(context.user, projectMemberId);
context.user,
projectMemberId,
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@@ -92,13 +78,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any, _: any,
{ {
projectMemberId, projectMemberId,
data, data
}: { }: {
projectMemberId: string; projectMemberId: string;
data: { data: {
permissions: Permission[]; permissions: Permission[];
}; };
}, }
) => { ) => {
try { try {
return await service.updateProjectMember(projectMemberId, data); return await service.updateProjectMember(projectMemberId, data);
@@ -112,14 +98,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any, _: any,
{ {
projectId, projectId,
data, data
}: { }: {
projectId: string; projectId: string;
data: { data: {
email: string; email: string;
permissions: Permission[]; permissions: Permission[];
}; };
}, }
) => { ) => {
try { try {
return Boolean(await service.addProjectMember(projectId, data)); return Boolean(await service.addProjectMember(projectId, data));
@@ -133,15 +119,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any, _: any,
{ {
projectId, projectId,
data, data
}: { }: {
projectId: string; projectId: string;
data: { environments: string[]; key: string; value: string }[]; data: { environments: string[]; key: string; value: string }[];
}, }
) => { ) => {
try { try {
return Boolean( return Boolean(
await service.addEnvironmentVariables(projectId, data), await service.addEnvironmentVariables(projectId, data)
); );
} catch (err) { } catch (err) {
log(err); log(err);
@@ -153,16 +139,16 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any, _: any,
{ {
environmentVariableId, environmentVariableId,
data, data
}: { }: {
environmentVariableId: string; environmentVariableId: string;
data: DeepPartial<EnvironmentVariable>; data: DeepPartial<EnvironmentVariable>;
}, }
) => { ) => {
try { try {
return await service.updateEnvironmentVariable( return await service.updateEnvironmentVariable(
environmentVariableId, environmentVariableId,
data, data
); );
} catch (err) { } catch (err) {
log(err); log(err);
@@ -172,7 +158,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
removeEnvironmentVariable: async ( removeEnvironmentVariable: async (
_: any, _: any,
{ environmentVariableId }: { environmentVariableId: string }, { environmentVariableId }: { environmentVariableId: string }
) => { ) => {
try { try {
return await service.removeEnvironmentVariable(environmentVariableId); return await service.removeEnvironmentVariable(environmentVariableId);
@@ -185,12 +171,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateDeploymentToProd: async ( updateDeploymentToProd: async (
_: any, _: any,
{ deploymentId }: { deploymentId: string }, { deploymentId }: { deploymentId: string },
context: any, context: any
) => { ) => {
try { try {
return Boolean( return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
await service.updateDeploymentToProd(context.user, deploymentId),
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@@ -201,9 +185,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any, _: any,
{ {
organizationSlug, organizationSlug,
data, data
}: { organizationSlug: string; data: DeepPartial<Project> }, }: { organizationSlug: string; data: DeepPartial<Project> },
context: any, context: any
) => { ) => {
try { try {
return await service.addProject(context.user, organizationSlug, data); return await service.addProject(context.user, organizationSlug, data);
@@ -215,7 +199,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateProject: async ( updateProject: async (
_: any, _: any,
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }, { projectId, data }: { projectId: string; data: DeepPartial<Project> }
) => { ) => {
try { try {
return await service.updateProject(projectId, data); return await service.updateProject(projectId, data);
@@ -228,12 +212,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
redeployToProd: async ( redeployToProd: async (
_: any, _: any,
{ deploymentId }: { deploymentId: string }, { deploymentId }: { deploymentId: string },
context: any, context: any
) => { ) => {
try { try {
return Boolean( return Boolean(await service.redeployToProd(context.user, deploymentId));
await service.redeployToProd(context.user, deploymentId),
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@@ -262,8 +244,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any, _: any,
{ {
projectId, projectId,
deploymentId, deploymentId
}: { deploymentId: string; projectId: string }, }: { deploymentId: string; projectId: string }
) => { ) => {
try { try {
return await service.rollbackDeployment(projectId, deploymentId); return await service.rollbackDeployment(projectId, deploymentId);
@@ -275,7 +257,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
deleteDeployment: async ( deleteDeployment: async (
_: any, _: any,
{ deploymentId }: { deploymentId: string }, {
deploymentId
}: { deploymentId: string; }
) => { ) => {
try { try {
return await service.deleteDeployment(deploymentId); return await service.deleteDeployment(deploymentId);
@@ -287,7 +271,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
addDomain: async ( addDomain: async (
_: any, _: any,
{ projectId, data }: { projectId: string; data: { name: string } }, { projectId, data }: { projectId: string; data: { name: string } }
) => { ) => {
try { try {
return Boolean(await service.addDomain(projectId, data)); return Boolean(await service.addDomain(projectId, data));
@@ -299,7 +283,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateDomain: async ( updateDomain: async (
_: any, _: any,
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }, { domainId, data }: { domainId: string; data: DeepPartial<Domain> }
) => { ) => {
try { try {
return await service.updateDomain(domainId, data); return await service.updateDomain(domainId, data);
@@ -312,7 +296,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
authenticateGitHub: async ( authenticateGitHub: async (
_: any, _: any,
{ code }: { code: string }, { code }: { code: string },
context: any, context: any
) => { ) => {
try { try {
return await service.authenticateGitHub(code, context.user); return await service.authenticateGitHub(code, context.user);
@@ -324,14 +308,12 @@ 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, { return service.unauthenticateGitHub(context.user, { gitHubToken: null });
gitHubToken: null,
});
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, }
}, }
}; };
}; };
-15
View File
@@ -5,19 +5,6 @@ import { authenticateUser, createUser } from '../turnkey-backend';
const router = Router(); const router = Router();
//
// Access Code
//
router.post('/accesscode', async (req, res) => {
console.log('Access Code', req.body);
const { accesscode } = req.body;
if (accesscode === '44444') {
return res.send({ isValid: true });
} else {
return res.sendStatus(204);
}
});
// //
// Turnkey // Turnkey
// //
@@ -32,7 +19,6 @@ router.get('/registration/:email', async (req, res) => {
}); });
router.post('/register', async (req, res) => { router.post('/register', async (req, res) => {
console.log('Register', req.body);
const { email, challenge, attestation } = req.body; const { email, challenge, attestation } = req.body;
const user = await createUser(req.app.get('service'), { const user = await createUser(req.app.get('service'), {
challenge, challenge,
@@ -45,7 +31,6 @@ router.post('/register', async (req, res) => {
}); });
router.post('/authenticate', async (req, res) => { router.post('/authenticate', async (req, res) => {
console.log('Authenticate', req.body);
const { signedWhoamiRequest } = req.body; const { signedWhoamiRequest } = req.body;
const user = await authenticateUser( const user = await authenticateUser(
req.app.get('service'), req.app.get('service'),
-9
View File
@@ -1,9 +0,0 @@
import { Router } from 'express';
const router = Router();
router.get('/version', async (req, res) => {
return res.send({ version: '0.0.8' });
});
export default router;
-7
View File
@@ -17,7 +17,6 @@ import { ServerConfig } from './config';
import { DEFAULT_GQL_PATH } from './constants'; import { DEFAULT_GQL_PATH } from './constants';
import githubRouter from './routes/github'; import githubRouter from './routes/github';
import authRouter from './routes/auth'; import authRouter from './routes/auth';
import stagingRouter from './routes/staging';
import { Service } from './service'; import { Service } from './service';
const log = debug('snowball:server'); const log = debug('snowball:server');
@@ -110,12 +109,6 @@ export const createAndStartServer = async (
app.set('service', service); app.set('service', service);
app.use('/auth', authRouter); app.use('/auth', authRouter);
app.use('/api/github', githubRouter); app.use('/api/github', githubRouter);
app.use('/staging', stagingRouter);
app.use((err: any, req: any, res: any, next: any) => {
console.error(err);
res.status(500).json({ error: err.message });
});
httpServer.listen(port, host, () => { httpServer.listen(port, host, () => {
log(`Server is listening on ${host}:${port}${server.graphqlPath}`); log(`Server is listening on ${host}:${port}${server.graphqlPath}`);
+169 -225
View File
@@ -15,12 +15,7 @@ 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 { import { AppDeploymentRecord, AppDeploymentRemovalRecord, GitPushEventPayload, PackageJSON } from './types';
AppDeploymentRecord,
AppDeploymentRemovalRecord,
GitPushEventPayload,
PackageJSON,
} from './types';
import { Role } from './entity/UserOrganization'; import { Role } from './entity/UserOrganization';
const log = debug('snowball:service'); const log = debug('snowball:service');
@@ -76,32 +71,33 @@ export class Service {
// 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
}, }
}); });
if (deployments.length) { if (deployments.length) {
log( log(
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`, `Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
); );
// Calculate a timestamp for one hour ago // Calculate a timestamp for one hour ago
const anHourAgo = Date.now() - HOUR; const anHourAgo = Date.now() - HOUR;
// Filter out deployments started more than an hour ago and mark them as Error // Filter out deployments started more than an hour ago and mark them as Error
const oldDeploymentsToUpdate = deployments const oldDeploymentsToUpdate = deployments.filter(
.filter((deployment) => Number(deployment.updatedAt) < anHourAgo) deployment => (Number(deployment.updatedAt) < anHourAgo)
)
.map((deployment) => { .map((deployment) => {
return this.db.updateDeploymentById(deployment.id, { return this.db.updateDeploymentById(deployment.id, {
status: DeploymentStatus.Error, status: DeploymentStatus.Error,
isCurrent: false, isCurrent: false
}); });
}); });
// If there are old deployments to update, log and perform the updates // If there are old deployments to update, log and perform the updates
if (oldDeploymentsToUpdate.length > 0) { if (oldDeploymentsToUpdate.length > 0) {
log( log(
`Cleaning up ${oldDeploymentsToUpdate.length} deployments stuck in ${DeploymentStatus.Building} state for over an hour`, `Cleaning up ${oldDeploymentsToUpdate.length} deployments stuck in ${DeploymentStatus.Building} state for over an hour`
); );
await Promise.all(oldDeploymentsToUpdate); await Promise.all(oldDeploymentsToUpdate);
} }
@@ -129,18 +125,17 @@ export class Service {
// Fetch deployments in deleting state // Fetch deployments in deleting state
const deployments = await this.db.getDeployments({ const deployments = await this.db.getDeployments({
where: { where: {
status: DeploymentStatus.Deleting, status: DeploymentStatus.Deleting
}, }
}); });
if (deployments.length) { if (deployments.length) {
log( log(
`Found ${deployments.length} deployments in ${DeploymentStatus.Deleting} state`, `Found ${deployments.length} deployments in ${DeploymentStatus.Deleting} state`
); );
// Fetch ApplicationDeploymentRemovalRecords for deployments // Fetch ApplicationDeploymentRemovalRecords for deployments
const records = const records = await this.registry.getDeploymentRemovalRecords(deployments);
await this.registry.getDeploymentRemovalRecords(deployments);
log(`Found ${records.length} ApplicationDeploymentRemovalRecords`); log(`Found ${records.length} ApplicationDeploymentRemovalRecords`);
// Update deployments for which ApplicationDeploymentRemovalRecords were returned // Update deployments for which ApplicationDeploymentRemovalRecords were returned
@@ -158,16 +153,16 @@ export class Service {
* Update deployments with ApplicationDeploymentRecord data * Update deployments with ApplicationDeploymentRecord data
*/ */
async updateDeploymentsWithRecordData ( async updateDeploymentsWithRecordData (
records: AppDeploymentRecord[], records: AppDeploymentRecord[]
): Promise<void> { ): Promise<void> {
// Get deployments for ApplicationDeploymentRecords // Get deployments for ApplicationDeploymentRecords
const deployments = await this.db.getDeployments({ const deployments = await this.db.getDeployments({
where: records.map((record) => ({ where: records.map((record) => ({
applicationRecordId: record.attributes.application, applicationRecordId: record.attributes.application
})), })),
order: { order: {
createdAt: 'DESC', createdAt: 'DESC'
}, }
}); });
// Get project IDs of deployments that are in production environment // Get project IDs of deployments that are in production environment
@@ -179,13 +174,13 @@ export class Service {
return acc; return acc;
}, },
new Set<string>(), new Set<string>()
); );
// Set old deployments isCurrent to false // Set old deployments isCurrent to false
await this.db.updateDeploymentsByProjectIds( await this.db.updateDeploymentsByProjectIds(
Array.from(productionDeploymentProjectIds), Array.from(productionDeploymentProjectIds),
{ isCurrent: false }, { isCurrent: false }
); );
const recordToDeploymentsMap = deployments.reduce( const recordToDeploymentsMap = deployments.reduce(
@@ -193,7 +188,7 @@ export class Service {
acc[deployment.applicationRecordId] = deployment; acc[deployment.applicationRecordId] = deployment;
return acc; return acc;
}, },
{}, {}
); );
// Update deployment data for ApplicationDeploymentRecords // Update deployment data for ApplicationDeploymentRecords
@@ -205,11 +200,11 @@ export class Service {
applicationDeploymentRecordData: record.attributes, applicationDeploymentRecordData: record.attributes,
url: record.attributes.url, url: record.attributes.url,
status: DeploymentStatus.Ready, status: DeploymentStatus.Ready,
isCurrent: deployment.environment === Environment.Production, isCurrent: deployment.environment === Environment.Production
}); });
log( log(
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`, `Updated deployment ${deployment.id} with URL ${record.attributes.url}`
); );
}); });
@@ -223,23 +218,17 @@ export class Service {
records: AppDeploymentRemovalRecord[], records: AppDeploymentRemovalRecord[],
deployments: Deployment[], deployments: Deployment[],
): Promise<void> { ): Promise<void> {
const removedApplicationDeploymentRecordIds = records.map( const removedApplicationDeploymentRecordIds = records.map(record => record.attributes.deployment);
(record) => record.attributes.deployment,
);
// Get removed deployments for ApplicationDeploymentRecords // Get removed deployments for ApplicationDeploymentRecords
const removedDeployments = deployments.filter((deployment) => const removedDeployments = deployments.filter(deployment => removedApplicationDeploymentRecordIds.includes(deployment.applicationDeploymentRecordId!))
removedApplicationDeploymentRecordIds.includes(
deployment.applicationDeploymentRecordId!,
),
);
const recordToDeploymentsMap = removedDeployments.reduce( const recordToDeploymentsMap = removedDeployments.reduce(
(acc: { [key: string]: Deployment }, deployment) => { (acc: { [key: string]: Deployment }, deployment) => {
acc[deployment.applicationDeploymentRecordId!] = deployment; acc[deployment.applicationDeploymentRecordId!] = deployment;
return acc; return acc;
}, },
{}, {}
); );
// Update deployment data for ApplicationDeploymentRecords and delete // Update deployment data for ApplicationDeploymentRecords and delete
@@ -252,10 +241,10 @@ export class Service {
}); });
log( log(
`Updated deployment ${deployment.id} with ApplicationDeploymentRemovalRecord ${record.id}`, `Updated deployment ${deployment.id} with ApplicationDeploymentRemovalRecord ${record.id}`
); );
await this.db.deleteDeploymentById(deployment.id); await this.db.deleteDeploymentById(deployment.id)
}); });
await Promise.all(deploymentUpdatePromises); await Promise.all(deploymentUpdatePromises);
@@ -264,41 +253,41 @@ export class Service {
async getUser (userId: string): Promise<User | null> { async getUser (userId: string): Promise<User | null> {
return this.db.getUser({ return this.db.getUser({
where: { where: {
id: userId, id: userId
}, }
}); });
} }
async getUserByEmail(email: string): Promise<User | null> { async getUserByEmail(email: string): Promise<User | null> {
return await this.db.getUser({ return await this.db.getUser({
where: { where: {
email, email
}, }
}); });
} }
async getUserBySubOrgId(subOrgId: string): Promise<User | null> { async getUserBySubOrgId(subOrgId: string): Promise<User | null> {
return await this.db.getUser({ return await this.db.getUser({
where: { where: {
subOrgId, subOrgId
}, }
}); });
} }
async getUserByEthAddress (ethAddress: string): Promise<User | null> { async getUserByEthAddress (ethAddress: string): Promise<User | null> {
return await this.db.getUser({ return await this.db.getUser({
where: { where: {
ethAddress, ethAddress
}, }
}); });
} }
async createUser (params: { async createUser (params: {
name: string; name: string
email: string; email: string
subOrgId: string; subOrgId: string
ethAddress: string; ethAddress: string
turnkeyWalletId: string; turnkeyWalletId: string
}): Promise<User> { }): Promise<User> {
const [org] = await this.db.getOrganizations({}); const [org] = await this.db.getOrganizations({});
assert(org, 'No organizations exists in database'); assert(org, 'No organizations exists in database');
@@ -316,7 +305,7 @@ export class Service {
await this.db.addUserOrganization({ await this.db.addUserOrganization({
member: user, member: user,
organization: org, organization: org,
role: Role.Owner, role: Role.Owner
}); });
return user; return user;
@@ -326,7 +315,7 @@ export class Service {
const user = await this.db.getUser({ where: { id: userId } }); const user = await this.db.getUser({ where: { id: userId } });
assert( assert(
user && user.gitHubToken, user && user.gitHubToken,
'User needs to be authenticated with GitHub token', 'User needs to be authenticated with GitHub token'
); );
return new Octokit({ auth: user.gitHubToken }); return new Octokit({ auth: user.gitHubToken });
@@ -342,14 +331,8 @@ export class Service {
return dbProject; return dbProject;
} }
async getProjectsInOrganization( async getProjectsInOrganization (user: User, organizationSlug: string): Promise<Project[]> {
user: User, const dbProjects = await this.db.getProjectsInOrganization(user.id, organizationSlug);
organizationSlug: string,
): Promise<Project[]> {
const dbProjects = await this.db.getProjectsInOrganization(
user.id,
organizationSlug,
);
return dbProjects; return dbProjects;
} }
@@ -359,7 +342,7 @@ export class Service {
} }
async getEnvironmentVariablesByProjectId ( async getEnvironmentVariablesByProjectId (
projectId: string, projectId: string
): Promise<EnvironmentVariable[]> { ): Promise<EnvironmentVariable[]> {
const dbEnvironmentVariables = const dbEnvironmentVariables =
await this.db.getEnvironmentVariablesByProjectId(projectId); await this.db.getEnvironmentVariablesByProjectId(projectId);
@@ -367,7 +350,7 @@ export class Service {
} }
async getProjectMembersByProjectId ( async getProjectMembersByProjectId (
projectId: string, projectId: string
): Promise<ProjectMember[]> { ): Promise<ProjectMember[]> {
const dbProjectMembers = const dbProjectMembers =
await this.db.getProjectMembersByProjectId(projectId); await this.db.getProjectMembersByProjectId(projectId);
@@ -375,16 +358,13 @@ export class Service {
} }
async searchProjects (user: User, searchText: string): Promise<Project[]> { async searchProjects (user: User, searchText: string): Promise<Project[]> {
const dbProjects = await this.db.getProjectsBySearchText( const dbProjects = await this.db.getProjectsBySearchText(user.id, searchText);
user.id,
searchText,
);
return dbProjects; return dbProjects;
} }
async getDomainsByProjectId ( async getDomainsByProjectId (
projectId: string, projectId: string,
filter?: FindOptionsWhere<Domain>, filter?: FindOptionsWhere<Domain>
): Promise<Domain[]> { ): Promise<Domain[]> {
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter); const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
return dbDomains; return dbDomains;
@@ -392,7 +372,7 @@ export class Service {
async updateProjectMember ( async updateProjectMember (
projectMemberId: string, projectMemberId: string,
data: { permissions: Permission[] }, data: { permissions: Permission[] }
): Promise<boolean> { ): Promise<boolean> {
return this.db.updateProjectMemberById(projectMemberId, data); return this.db.updateProjectMemberById(projectMemberId, data);
} }
@@ -402,39 +382,36 @@ export class Service {
data: { data: {
email: string; email: string;
permissions: Permission[]; permissions: Permission[];
}, }
): Promise<ProjectMember> { ): Promise<ProjectMember> {
// TODO: Send invitation // TODO: Send invitation
let user = await this.db.getUser({ let user = await this.db.getUser({
where: { where: {
email: data.email, email: data.email
}, }
}); });
if (!user) { if (!user) {
user = await this.db.addUser({ user = await this.db.addUser({
email: data.email, email: data.email
}); });
} }
const newProjectMember = await this.db.addProjectMember({ const newProjectMember = await this.db.addProjectMember({
project: { project: {
id: projectId, id: projectId
}, },
permissions: data.permissions, permissions: data.permissions,
isPending: true, isPending: true,
member: { member: {
id: user.id, id: user.id
}, }
}); });
return newProjectMember; return newProjectMember;
} }
async removeProjectMember( async removeProjectMember (user: User, projectMemberId: string): Promise<boolean> {
user: User,
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) === user.id) {
@@ -453,7 +430,7 @@ export class Service {
async addEnvironmentVariables ( async addEnvironmentVariables (
projectId: string, projectId: string,
data: { environments: string[]; key: string; value: string }[], data: { environments: string[]; key: string; value: string }[]
): Promise<EnvironmentVariable[]> { ): Promise<EnvironmentVariable[]> {
const formattedEnvironmentVariables = data const formattedEnvironmentVariables = data
.map((environmentVariable) => { .map((environmentVariable) => {
@@ -463,41 +440,38 @@ export class Service {
value: environmentVariable.value, value: environmentVariable.value,
environment: environment as Environment, environment: environment as Environment,
project: Object.assign(new Project(), { project: Object.assign(new Project(), {
id: projectId, id: projectId
}), })
}; };
}); });
}) })
.flat(); .flat();
const savedEnvironmentVariables = await this.db.addEnvironmentVariables( const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
formattedEnvironmentVariables, formattedEnvironmentVariables
); );
return savedEnvironmentVariables; return savedEnvironmentVariables;
} }
async updateEnvironmentVariable ( async updateEnvironmentVariable (
environmentVariableId: string, environmentVariableId: string,
data: DeepPartial<EnvironmentVariable>, data: DeepPartial<EnvironmentVariable>
): Promise<boolean> { ): Promise<boolean> {
return this.db.updateEnvironmentVariable(environmentVariableId, data); return this.db.updateEnvironmentVariable(environmentVariableId, data);
} }
async removeEnvironmentVariable ( async removeEnvironmentVariable (
environmentVariableId: string, environmentVariableId: string
): Promise<boolean> { ): Promise<boolean> {
return this.db.deleteEnvironmentVariable(environmentVariableId); return this.db.deleteEnvironmentVariable(environmentVariableId);
} }
async updateDeploymentToProd( async updateDeploymentToProd (user: User, deploymentId: string): Promise<Deployment> {
user: User,
deploymentId: string,
): Promise<Deployment> {
const oldDeployment = await this.db.getDeployment({ const oldDeployment = await this.db.getDeployment({
where: { id: deploymentId }, where: { id: deploymentId },
relations: { relations: {
project: true, project: true
}, }
}); });
if (!oldDeployment) { if (!oldDeployment) {
@@ -506,18 +480,20 @@ export class Service {
const prodBranchDomains = await this.db.getDomainsByProjectId( const prodBranchDomains = await this.db.getDomainsByProjectId(
oldDeployment.project.id, oldDeployment.project.id,
{ branch: oldDeployment.project.prodBranch }, { branch: oldDeployment.project.prodBranch }
); );
const octokit = await this.getOctokit(user.id); const octokit = await this.getOctokit(user.id);
const newDeployment = await this.createDeployment(user.id, octokit, { const newDeployment = await this.createDeployment(user.id,
octokit,
{
project: oldDeployment.project, project: oldDeployment.project,
branch: oldDeployment.branch, branch: oldDeployment.branch,
environment: Environment.Production, environment: Environment.Production,
domain: prodBranchDomains[0], domain: prodBranchDomains[0],
commitHash: oldDeployment.commitHash, commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage, commitMessage: oldDeployment.commitMessage
}); });
return newDeployment; return newDeployment;
@@ -526,11 +502,11 @@ export class Service {
async createDeployment ( async createDeployment (
userId: string, userId: string,
octokit: Octokit, octokit: Octokit,
data: DeepPartial<Deployment>, data: DeepPartial<Deployment>
): Promise<Deployment> { ): Promise<Deployment> {
assert(data.project?.repository, 'Project repository not found'); assert(data.project?.repository, 'Project repository not found');
log( log(
`Creating deployment in project ${data.project.name} from branch ${data.branch}`, `Creating deployment in project ${data.project.name} from branch ${data.branch}`
); );
const [owner, repo] = data.project.repository.split('/'); const [owner, repo] = data.project.repository.split('/');
@@ -538,7 +514,7 @@ export class Service {
owner, owner,
repo, repo,
path: 'package.json', path: 'package.json',
ref: data.commitHash, ref: data.commitHash
}); });
if (!packageJSONData) { if (!packageJSONData) {
@@ -550,12 +526,10 @@ 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 = ( const repoUrl = (await octokit.rest.repos.get({
await octokit.rest.repos.get({
owner, owner,
repo, repo
}) })).data.html_url;
).data.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 } =
@@ -564,20 +538,17 @@ export class Service {
packageJSON, packageJSON,
appType: data.project!.template!, appType: data.project!.template!,
commitHash: data.commitHash!, commitHash: data.commitHash!,
repoUrl, repoUrl
}); });
// 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) { 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({
@@ -591,67 +562,53 @@ export class Service {
applicationRecordData, applicationRecordData,
domain: data.domain, domain: data.domain,
createdBy: Object.assign(new User(), { createdBy: Object.assign(new User(), {
id: userId, id: userId
}), })
}); });
log( log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`);
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`,
);
const environmentVariables = const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(data.project.id!, { environment: Environment.Production });
await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
environment: Environment.Production,
});
const environmentVariablesObj = environmentVariables.reduce( const environmentVariablesObj = environmentVariables.reduce((acc, env) => {
(acc, env) => {
acc[env.key] = env.value; acc[env.key] = env.value;
return acc; return acc;
}, }, {} as { [key: string]: string });
{} as { [key: string]: string },
);
// To set project DNS // To set project DNS
if (data.environment === Environment.Production) { if (data.environment === Environment.Production) {
// On deleting deployment later, project DNS deployment is also deleted // 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 // So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
await this.registry.createApplicationDeploymentRequest({ await this.registry.createApplicationDeploymentRequest(
{
deployment: newDeployment, deployment: newDeployment,
appName: repo, appName: repo,
repository: repoUrl, repository: repoUrl,
environmentVariables: environmentVariablesObj, environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`, dns: `${newDeployment.project.name}`
}); });
} }
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
await this.registry.createApplicationDeploymentRequest({ {
deployment: newDeployment, deployment: newDeployment,
appName: repo, appName: repo,
repository: repoUrl, repository: repoUrl,
environmentVariables: environmentVariablesObj, environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`, dns: `${newDeployment.project.name}-${newDeployment.id}`
}); });
await this.db.updateDeploymentById(newDeployment.id, { await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
applicationDeploymentRequestId,
applicationDeploymentRequestData,
});
return newDeployment; return newDeployment;
} }
async addProject( async addProject (user: User, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
user: User,
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
}, }
}); });
if (!organization) { if (!organization) {
throw new Error('Organization does not exist'); throw new Error('Organization does not exist');
@@ -663,23 +620,26 @@ export class Service {
const [owner, repo] = project.repository.split('/'); const [owner, repo] = project.repository.split('/');
const { const {
data: [latestCommit], data: [latestCommit]
} = await octokit.rest.repos.listCommits({ } = await octokit.rest.repos.listCommits({
owner, owner,
repo, repo,
sha: project.prodBranch, sha: project.prodBranch,
per_page: 1, per_page: 1
}); });
// Create deployment with prod branch and latest commit // Create deployment with prod branch and latest commit
await this.createDeployment(user.id, octokit, { await this.createDeployment(user.id,
octokit,
{
project, project,
branch: project.prodBranch, branch: project.prodBranch,
environment: Environment.Production, environment: Environment.Production,
domain: null, domain: null,
commitHash: latestCommit.sha, commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message, commitMessage: latestCommit.commit.message
}); }
);
await this.createRepoHook(octokit, project); await this.createRepoHook(octokit, project);
@@ -695,11 +655,11 @@ export class Service {
config: { config: {
url: new URL( url: new URL(
'api/github/webhook', 'api/github/webhook',
this.config.gitHubConfig.webhookUrl, this.config.gitHubConfig.webhookUrl
).href, ).href,
content_type: 'json', content_type: 'json'
}, },
events: ['push'], events: ['push']
}); });
} catch (err) { } catch (err) {
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes // https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
@@ -708,7 +668,7 @@ export class Service {
err instanceof RequestError && err instanceof RequestError &&
err.status === 422 && err.status === 422 &&
(err.response?.data as any).errors.some( (err.response?.data as any).errors.some(
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR, (err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
) )
) )
) { ) {
@@ -727,11 +687,9 @@ export class Service {
return; return;
} }
log( log(`Handling GitHub push event from repository: ${repository.full_name}, branch: ${ref}`);
`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 }
}); });
if (!projects.length) { if (!projects.length) {
@@ -745,7 +703,7 @@ export class Service {
for await (const project of projects) { for await (const project of projects) {
const octokit = await this.getOctokit(project.ownerId); const octokit = await this.getOctokit(project.ownerId);
const [domain] = await this.db.getDomainsByProjectId(project.id, { const [domain] = await this.db.getDomainsByProjectId(project.id, {
branch, branch
}); });
// Create deployment with branch and latest commit in GitHub data // Create deployment with branch and latest commit in GitHub data
@@ -758,14 +716,14 @@ export class Service {
: Environment.Preview, : Environment.Preview,
domain, domain,
commitHash: headCommit.id, commitHash: headCommit.id,
commitMessage: headCommit.message, commitMessage: headCommit.message
}); });
} }
} }
async updateProject ( async updateProject (
projectId: string, projectId: string,
data: DeepPartial<Project>, data: DeepPartial<Project>
): Promise<boolean> { ): Promise<boolean> {
return this.db.updateProjectById(projectId, data); return this.db.updateProjectById(projectId, data);
} }
@@ -778,13 +736,13 @@ export class Service {
async deleteDomain (domainId: string): Promise<boolean> { async deleteDomain (domainId: string): Promise<boolean> {
const domainsRedirectedFrom = await this.db.getDomains({ const domainsRedirectedFrom = await this.db.getDomains({
where: { where: {
redirectToId: domainId, redirectToId: domainId
}, }
}); });
if (domainsRedirectedFrom.length > 0) { if (domainsRedirectedFrom.length > 0) {
throw new Error( throw new Error(
'Cannot delete domain since it has redirects from other domains', 'Cannot delete domain since it has redirects from other domains'
); );
} }
@@ -796,11 +754,11 @@ export class Service {
relations: { relations: {
project: true, project: true,
domain: true, domain: true,
createdBy: true, createdBy: true
}, },
where: { where: {
id: deploymentId, id: deploymentId
}, }
}); });
if (oldDeployment === null) { if (oldDeployment === null) {
@@ -809,14 +767,16 @@ export class Service {
const octokit = await this.getOctokit(user.id); const octokit = await this.getOctokit(user.id);
const newDeployment = await this.createDeployment(user.id, octokit, { const newDeployment = await this.createDeployment(user.id,
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,
environment: Environment.Production, environment: Environment.Production,
domain: oldDeployment.domain, domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash, commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage, commitMessage: oldDeployment.commitMessage
}); });
return newDeployment; return newDeployment;
@@ -824,19 +784,19 @@ export class Service {
async rollbackDeployment ( async rollbackDeployment (
projectId: string, projectId: string,
deploymentId: string, deploymentId: string
): Promise<boolean> { ): Promise<boolean> {
// TODO: Implement transactions // TODO: Implement transactions
const oldCurrentDeployment = await this.db.getDeployment({ const oldCurrentDeployment = await this.db.getDeployment({
relations: { relations: {
domain: true, domain: true
}, },
where: { where: {
project: { project: {
id: projectId, id: projectId
},
isCurrent: true,
}, },
isCurrent: true
}
}); });
if (!oldCurrentDeployment) { if (!oldCurrentDeployment) {
@@ -845,12 +805,12 @@ export class Service {
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById( const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(
oldCurrentDeployment.id, oldCurrentDeployment.id,
{ isCurrent: false, domain: null }, { isCurrent: false, domain: null }
); );
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById( const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(
deploymentId, deploymentId,
{ isCurrent: true, domain: oldCurrentDeployment?.domain }, { isCurrent: true, domain: oldCurrentDeployment?.domain }
); );
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate; return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
@@ -859,11 +819,11 @@ export class Service {
async deleteDeployment (deploymentId: string): Promise<boolean> { async deleteDeployment (deploymentId: string): Promise<boolean> {
const deployment = await this.db.getDeployment({ const deployment = await this.db.getDeployment({
where: { where: {
id: deploymentId, id: deploymentId
}, },
relations: { relations: {
project: true, project: true
}, }
}); });
if (deployment && deployment.applicationDeploymentRecordId) { if (deployment && deployment.applicationDeploymentRecordId) {
@@ -871,39 +831,31 @@ export class Service {
if (deployment.isCurrent) { if (deployment.isCurrent) {
const currentDeploymentURL = `https://${deployment.project.subDomain}`; const currentDeploymentURL = `https://${deployment.project.subDomain}`;
const deploymentRecords = const deploymentRecords = await this.registry.getDeploymentRecordsByFilter({
await this.registry.getDeploymentRecordsByFilter({
application: deployment.applicationRecordId, application: deployment.applicationRecordId,
url: currentDeploymentURL, url: currentDeploymentURL
}); })
if (!deploymentRecords.length) { if (!deploymentRecords.length) {
log( log(`No ApplicationDeploymentRecord found for URL ${currentDeploymentURL} and ApplicationDeploymentRecord id ${deployment.applicationDeploymentRecordId}`);
`No ApplicationDeploymentRecord found for URL ${currentDeploymentURL} and ApplicationDeploymentRecord id ${deployment.applicationDeploymentRecordId}`,
);
return false; return false;
} }
await this.registry.createApplicationDeploymentRemovalRequest({ await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deploymentRecords[0].id });
deploymentId: deploymentRecords[0].id,
});
} }
const result = const result = await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deployment.applicationDeploymentRecordId });
await this.registry.createApplicationDeploymentRemovalRequest({
deploymentId: deployment.applicationDeploymentRecordId,
});
await this.db.updateDeploymentById(deployment.id, { await this.db.updateDeploymentById(
deployment.id,
{
status: DeploymentStatus.Deleting, status: DeploymentStatus.Deleting,
applicationDeploymentRemovalRequestId: applicationDeploymentRemovalRequestId: result.applicationDeploymentRemovalRequestId,
result.applicationDeploymentRemovalRequestId, applicationDeploymentRemovalRequestData: result.applicationDeploymentRemovalRequestData
applicationDeploymentRemovalRequestData: }
result.applicationDeploymentRemovalRequestData, );
});
return result !== undefined || result !== null; return (result !== undefined || result !== null);
} }
return false; return false;
@@ -911,7 +863,7 @@ export class Service {
async addDomain ( async addDomain (
projectId: string, projectId: string,
data: { name: string }, data: { name: string }
): Promise<{ ): Promise<{
primaryDomain: Domain; primaryDomain: Domain;
redirectedDomain: Domain; redirectedDomain: Domain;
@@ -925,7 +877,7 @@ export class Service {
const primaryDomainDetails = { const primaryDomainDetails = {
...data, ...data,
branch: currentProject.prodBranch, branch: currentProject.prodBranch,
project: currentProject, project: currentProject
}; };
const savedPrimaryDomain = await this.db.addDomain(primaryDomainDetails); const savedPrimaryDomain = await this.db.addDomain(primaryDomainDetails);
@@ -936,27 +888,27 @@ export class Service {
name: domainArr.length > 1 ? domainArr[1] : `www.${domainArr[0]}`, name: domainArr.length > 1 ? domainArr[1] : `www.${domainArr[0]}`,
branch: currentProject.prodBranch, branch: currentProject.prodBranch,
project: currentProject, project: currentProject,
redirectTo: savedPrimaryDomain, redirectTo: savedPrimaryDomain
}; };
const savedRedirectedDomain = await this.db.addDomain( const savedRedirectedDomain = await this.db.addDomain(
redirectedDomainDetails, redirectedDomainDetails
); );
return { return {
primaryDomain: savedPrimaryDomain, primaryDomain: savedPrimaryDomain,
redirectedDomain: savedRedirectedDomain, redirectedDomain: savedRedirectedDomain
}; };
} }
async updateDomain ( async updateDomain (
domainId: string, domainId: string,
data: DeepPartial<Domain>, data: DeepPartial<Domain>
): Promise<boolean> { ): Promise<boolean> {
const domain = await this.db.getDomain({ const domain = await this.db.getDomain({
where: { where: {
id: domainId, id: domainId
}, }
}); });
if (domain === null) { if (domain === null) {
@@ -964,16 +916,16 @@ export class Service {
} }
const newDomain = { const newDomain = {
...data, ...data
}; };
const domainsRedirectedFrom = await this.db.getDomains({ const domainsRedirectedFrom = await this.db.getDomains({
where: { where: {
project: { project: {
id: domain.projectId, id: domain.projectId
},
redirectToId: domain.id,
}, },
redirectToId: domain.id
}
}); });
// If there are domains redirecting to current domain, only branch of current domain can be updated // If there are domains redirecting to current domain, only branch of current domain can be updated
@@ -984,8 +936,8 @@ export class Service {
if (data.redirectToId) { if (data.redirectToId) {
const redirectedDomain = await this.db.getDomain({ const redirectedDomain = await this.db.getDomain({
where: { where: {
id: data.redirectToId, id: data.redirectToId
}, }
}); });
if (redirectedDomain === null) { if (redirectedDomain === null) {
@@ -994,7 +946,7 @@ export class Service {
if (redirectedDomain.redirectToId) { if (redirectedDomain.redirectToId) {
throw new Error( throw new Error(
'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.', 'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.'
); );
} }
@@ -1006,14 +958,9 @@ export class Service {
return updateResult; return updateResult;
} }
async authenticateGitHub( async authenticateGitHub (code:string, user: User): Promise<{token: string}> {
code: string, const { authentication: { token } } = await this.oauthApp.createToken({
user: User, code
): Promise<{ token: string }> {
const {
authentication: { token },
} = await this.oauthApp.createToken({
code,
}); });
await this.db.updateUser(user, { gitHubToken: token }); await this.db.updateUser(user, { gitHubToken: token });
@@ -1021,10 +968,7 @@ export class Service {
return { token }; return { token };
} }
async unauthenticateGitHub( async unauthenticateGitHub (user: User, data: DeepPartial<User>): Promise<boolean> {
user: User,
data: DeepPartial<User>,
): Promise<boolean> {
return this.db.updateUser(user, data); return this.db.updateUser(user, data);
} }
} }
+2 -9
View File
@@ -1,11 +1,9 @@
# deployer # deployer
- Install dependencies - Install dependencies
```bash ```bash
yarn yarn
``` ```
```bash ```bash
brew install jq # if you do not have jq installed already brew install jq # if you do not have jq installed already
``` ```
@@ -14,13 +12,13 @@
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com` - To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
```bash ```
./deploy-frontend.staging.sh ./deploy-frontend.staging.sh
``` ```
- To deploy frontend app to `dashboard.apps.snowballtools.com` - To deploy frontend app to `dashboard.apps.snowballtools.com`
```bash ```
./deploy-frontend.sh ./deploy-frontend.sh
``` ```
@@ -29,7 +27,6 @@
## Notes ## Notes
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml) - Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
```yml ```yml
record: record:
... ...
@@ -38,7 +35,6 @@
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com 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 - On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
## Troubleshoot ## Troubleshoot
@@ -48,7 +44,6 @@
- If deployment fails due to low bond balance - If deployment fails due to low bond balance
- Check balances - Check balances
```bash ```bash
# Account balance # Account balance
yarn laconic cns account get yarn laconic cns account get
@@ -56,9 +51,7 @@
# Bond balance # Bond balance
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
``` ```
- Command to refill bond - Command to refill bond
```bash ```bash
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000 yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
``` ```
+2 -2
View File
@@ -3,7 +3,7 @@ services:
restEndpoint: 'http://console.laconic.com:1317' restEndpoint: 'http://console.laconic.com:1317'
gqlEndpoint: 'http://console.laconic.com:9473/api' gqlEndpoint: 'http://console.laconic.com:9473/api'
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb bondId: 098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661
chainId: laconic_9000-1 chainId: laconic_9000-1
gas: 1200000 gas: 550000
fees: 200000aphoton fees: 200000aphoton
+6 -38
View File
@@ -37,9 +37,9 @@ record:
env: env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/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/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_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta: meta:
note: Added by Snowball @ $CURRENT_DATE_TIME note: Added by Snowball @ $CURRENT_DATE_TIME
@@ -64,13 +64,7 @@ echo "Files generated successfully."
RECORD_FILE=records/application-record.yml RECORD_FILE=records/application-record.yml
# Publish ApplicationRecord # Publish ApplicationRecord
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE) RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to publish record"
exit $rc
fi
RECORD_ID=$(echo $publish_response | jq -r '.id')
echo "ApplicationRecord published" echo "ApplicationRecord published"
echo $RECORD_ID echo $RECORD_ID
@@ -79,36 +73,15 @@ REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
sleep 2 sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set name: $REGISTRY_APP_CRN@${PACKAGE_VERSION}"
exit $rc
fi
sleep 2 sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set hash"
exit $rc
fi
sleep 2 sleep 2
# Set name if latest release # Set name if latest release
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set release"
exit $rc
fi
echo "$REGISTRY_APP_CRN set for ApplicationRecord" echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN # Check if record found for REGISTRY_APP_CRN
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN") APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query name"
exit $rc
fi
APP_RECORD=$(echo $query_response | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN." echo "No record found for $REGISTRY_APP_CRN."
exit 1 exit 1
@@ -117,11 +90,6 @@ fi
RECORD_FILE=records/application-deployment-request.yml RECORD_FILE=records/application-deployment-request.yml
sleep 2 sleep 2
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE) DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query deployment request"
exit $rc
fi
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
echo "ApplicationDeploymentRequest published" echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID echo $DEPLOYMENT_REQUEST_ID
+8 -40
View File
@@ -36,16 +36,16 @@ record:
config: config:
env: env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_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/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_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball 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_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073 LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591 LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta: meta:
note: Added by Snowball @ $CURRENT_DATE_TIME note: Added by Snowball @ $CURRENT_DATE_TIME
@@ -70,13 +70,7 @@ echo "Files generated successfully."
RECORD_FILE=staging-records/application-record.yml RECORD_FILE=staging-records/application-record.yml
# Publish ApplicationRecord # Publish ApplicationRecord
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE) RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to publish record"
exit $rc
fi
RECORD_ID=$(echo $publish_response | jq -r '.id')
echo "ApplicationRecord published" echo "ApplicationRecord published"
echo $RECORD_ID echo $RECORD_ID
@@ -85,36 +79,15 @@ REGISTRY_APP_CRN="crn://staging-snowballtools/applications/staging-snowballtools
sleep 2 sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set name: $REGISTRY_APP_CRN@${PACKAGE_VERSION}"
exit $rc
fi
sleep 2 sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set hash"
exit $rc
fi
sleep 2 sleep 2
# Set name if latest release # Set name if latest release
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set release"
exit $rc
fi
echo "$REGISTRY_APP_CRN set for ApplicationRecord" echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN # Check if record found for REGISTRY_APP_CRN
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN") APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query name"
exit $rc
fi
APP_RECORD=$(echo $query_response | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN." echo "No record found for $REGISTRY_APP_CRN."
exit 1 exit 1
@@ -123,11 +96,6 @@ fi
RECORD_FILE=staging-records/application-deployment-request.yml RECORD_FILE=staging-records/application-deployment-request.yml
sleep 2 sleep 2
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE) DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query deployment request"
exit $rc
fi
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
echo "ApplicationDeploymentRequest published" echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID echo $DEPLOYMENT_REQUEST_ID
@@ -1,6 +1,6 @@
record: record:
type: ApplicationDeploymentRequest type: ApplicationDeploymentRequest
version: "1.0.0" version: '1.0.0'
name: snowballtools-base-frontend@0.1.8 name: snowballtools-base-frontend@0.1.8
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8 application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
dns: dashboard dns: dashboard
@@ -8,9 +8,9 @@ record:
env: env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com 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_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools/test-progressive-web-app LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools/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/image-upload-pwa-example LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2 LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta: meta:
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024 note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
@@ -7,18 +7,18 @@ record:
config: config:
env: env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_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/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_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball 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_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073 LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591 LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta: meta:
note: Added by Snowball @ Sat Jun 22 02:49:45 UTC 2024 note: Added by Snowball @ Tuesday 21 May 2024 06:17:23 AM UTC
repository: "https://git.vdb.to/cerc-io/snowballtools-base" repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601 repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
@@ -1,7 +1,7 @@
record: record:
type: ApplicationRecord type: ApplicationRecord
version: 0.0.1 version: 0.0.2
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601 repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"] repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp app_type: webapp
name: staging-snowballtools-base-frontend name: staging-snowballtools-base-frontend
+1 -1
View File
@@ -2,7 +2,7 @@
Check if the live web app deployer is in a working state Check if the live web app deployer is in a working state
- Web app repo used: <https://github.com/snowball-tools/test-progressive-web-app> (main branch) - Web app repo used: https://github.com/snowball-tools-platform/test-progressive-web-app (main branch)
- Config used: [../config.yml](../config.yml) - Config used: [../config.yml](../config.yml)
- The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following: - 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 - Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo
@@ -1,6 +1,6 @@
record: record:
type: ApplicationDeploymentRequest type: ApplicationDeploymentRequest
version: "1.0.0" version: '1.0.0'
name: deployment-test-app@0.1.24 name: deployment-test-app@0.1.24
application: crn://snowballtools/applications/deployment-test-app@0.1.24 application: crn://snowballtools/applications/deployment-test-app@0.1.24
dns: deployment-ci-test dns: deployment-ci-test
@@ -11,5 +11,5 @@ record:
CERC_WEBAPP_DEBUG: 0 CERC_WEBAPP_DEBUG: 0
meta: meta:
note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC
repository: "https://github.com/snowball-tools/test-progressive-web-app" repository: "https://github.com/snowball-tools-platform/test-progressive-web-app"
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26 repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
@@ -2,7 +2,7 @@ record:
type: ApplicationRecord type: ApplicationRecord
version: 0.0.1 version: 0.0.1
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26 repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
repository: ["https://github.com/snowball-tools/test-progressive-web-app"] repository: ["https://github.com/snowball-tools-platform/test-progressive-web-app"]
app_type: webapp app_type: webapp
name: deployment-test-app name: deployment-test-app
app_version: 0.1.24 app_version: 0.1.24
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Repository URL # Repository URL
REPO_URL="https://github.com/snowball-tools/test-progressive-web-app" REPO_URL="https://github.com/snowball-tools-platform/test-progressive-web-app"
# Get the latest commit hash from the repository # Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}') LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
-2
View File
@@ -9,7 +9,6 @@ import { join, dirname } from 'path';
function getAbsolutePath(value: string): any { function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json'))); return dirname(require.resolve(join(value, 'package.json')));
} }
const config: StorybookConfig = { const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [ addons: [
@@ -27,7 +26,6 @@ const config: StorybookConfig = {
docs: { docs: {
autodocs: 'tag', autodocs: 'tag',
}, },
staticDirs: ['../public'],
}; };
export default config; export default config;
+23 -56
View File
@@ -1,63 +1,30 @@
# frontend # React + TypeScript + Vite
This is a [vite](https://vitejs.dev/) [react](https://reactjs.org/) [nextjs](https://nextjs.org/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces). This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
## Getting Started Currently, two official plugins are available:
### Install dependencies - [@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
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
In the root of the project, run: ## Expanding the ESLint configuration
```zsh If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
yarn
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
};
``` ```
### Build backend - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
```zsh - 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 --ignore-frontend
```
### Environment variables
#### Local
Copy the `.env.example` file to `.env`:
```zsh
cp .env.example .env
```
#### Staging environment variables
Change in [deployer/deploy-frontend.staging.sh](/packages/deployer/deploy-frontend.staging.sh)
#### Production environment variables
Change in [deployer/deploy-frontend.sh](/packages/deployer/deploy-frontend.sh)
### Run development server
```zsh
yarn dev
```
## Deployment
From the root of the project, run:
### Staging
```zsh
cd packages/deployer && ./deploy-frontend.staging.sh
```
### Production
```zsh
cd packages/deployer && ./deploy-frontend.sh
```
### Deployment status
Check the status of the deployment [here](https://webapp-deployer.apps.snowballtools.com)
+2 -3
View File
@@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Projects from './pages/org-slug'; import Projects from './pages/org-slug';
@@ -11,8 +10,8 @@ import ProjectSearchLayout from './layouts/ProjectSearch';
import Index from './pages'; import Index from './pages';
import AuthPage from './pages/AuthPage'; import AuthPage from './pages/AuthPage';
import { DashboardLayout } from './pages/org-slug/layout'; import { DashboardLayout } from './pages/org-slug/layout';
import { useEffect } from 'react';
import Web3Provider from 'context/Web3Provider'; import Web3Provider from 'context/Web3Provider';
import { baseUrl } from 'utils/constants';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -60,7 +59,7 @@ function App() {
// Hacky way of checking session // Hacky way of checking session
// TODO: Handle redirect backs // TODO: Handle redirect backs
useEffect(() => { useEffect(() => {
fetch(`${baseUrl}/auth/session`, { fetch(`${import.meta.env.VITE_SERVER_URL}/auth/session`, {
credentials: 'include', credentials: 'include',
}).then((res) => { }).then((res) => {
if (res.status !== 200) { if (res.status !== 200) {
+2 -7
View File
@@ -1,21 +1,16 @@
import {
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO,
VITE_GITHUB_PWA_TEMPLATE_REPO,
} from 'utils/constants';
export default [ export default [
{ {
id: '1', id: '1',
name: 'Progressive Web App (PWA)', name: 'Progressive Web App (PWA)',
icon: 'pwa', icon: 'pwa',
repoFullName: `${VITE_GITHUB_PWA_TEMPLATE_REPO}`, repoFullName: `${import.meta.env.VITE_GITHUB_PWA_TEMPLATE_REPO}`,
isComingSoon: false, isComingSoon: false,
}, },
{ {
id: '2', id: '2',
name: 'Image Upload PWA', name: 'Image Upload PWA',
icon: 'pwa', icon: 'pwa',
repoFullName: `${VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`, repoFullName: `${import.meta.env.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
isComingSoon: false, isComingSoon: false,
}, },
{ {
@@ -14,10 +14,11 @@ import { useToast } from '../../shared/Toast';
import { IconWithFrame } from '../../shared/IconWithFrame'; import { IconWithFrame } from '../../shared/IconWithFrame';
import { Heading } from '../../shared/Heading'; import { Heading } from '../../shared/Heading';
import { MockConnectGitCard } from './MockConnectGitCard'; import { MockConnectGitCard } from './MockConnectGitCard';
import { VITE_GITHUB_CLIENT_ID } from 'utils/constants';
const SCOPES = 'repo user'; const SCOPES = 'repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`; const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
import.meta.env.VITE_GITHUB_CLIENT_ID
}&scope=${encodeURIComponent(SCOPES)}`;
interface ConnectAccountInterface { interface ConnectAccountInterface {
onAuth: (token: string) => void; onAuth: (token: string) => void;
@@ -1,8 +1,13 @@
import { CopyBlock, atomOneLight } from 'react-code-blocks'; import { CopyBlock, atomOneLight } from 'react-code-blocks';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Modal } from 'components/shared/Modal'; import {
import { Button } from 'components/shared/Button'; Button,
Dialog,
DialogHeader,
DialogBody,
DialogFooter,
} from '@snowballtools/material-tailwind-react-fork';
interface AssignDomainProps { interface AssignDomainProps {
open: boolean; open: boolean;
@@ -11,12 +16,11 @@ interface AssignDomainProps {
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => { const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
return ( return (
<Modal open={open} onOpenChange={handleOpen}> <Dialog open={open} handler={handleOpen}>
<Modal.Content> <DialogHeader>Assign Domain</DialogHeader>
<Modal.Header>Assign Domain</Modal.Header> <DialogBody>
<Modal.Body> In order to assign a domain to your production deployments, configure it
In order to assign a domain to your production deployments, configure in the{' '}
it in the{' '}
{/* TODO: Fix selection of project settings tab on navigation to domains */} {/* TODO: Fix selection of project settings tab on navigation to domains */}
<Link to="../settings/domains" className="text-light-blue-800 inline"> <Link to="../settings/domains" className="text-light-blue-800 inline">
project settings{' '} project settings{' '}
@@ -30,12 +34,18 @@ const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
showLineNumbers={false} showLineNumbers={false}
theme={atomOneLight} theme={atomOneLight}
/> />
</Modal.Body> </DialogBody>
<Modal.Footer className="flex justify-start"> <DialogFooter className="flex justify-start">
<Button onClick={handleOpen}>Okay</Button> <Button
</Modal.Footer> className="rounded-3xl"
</Modal.Content> variant="gradient"
</Modal> color="blue"
onClick={handleOpen}
>
<span>Okay</span>
</Button>
</DialogFooter>
</Dialog>
); );
}; };
@@ -1,12 +1,5 @@
import { useState, ComponentPropsWithRef } from 'react'; import { useState } from 'react';
import toast from 'react-hot-toast';
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@snowballtools/material-tailwind-react-fork';
import { Deployment, Domain, Environment, Project } from 'gql-client'; import { Deployment, Domain, Environment, Project } from 'gql-client';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { import {
@@ -18,11 +11,17 @@ import {
UndoIcon, UndoIcon,
CrossCircleIcon, CrossCircleIcon,
} from 'components/shared/CustomIcon'; } from 'components/shared/CustomIcon';
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@snowballtools/material-tailwind-react-fork';
import { ComponentPropsWithRef } from 'react';
import AssignDomainDialog from './AssignDomainDialog'; import AssignDomainDialog from './AssignDomainDialog';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { cn } from 'utils/classnames'; import { cn } from 'utils/classnames';
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog'; import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
import { useToast } from 'components/shared/Toast';
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> { interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
deployment: Deployment; deployment: Deployment;
@@ -42,7 +41,6 @@ export const DeploymentMenu = ({
...props ...props
}: DeploymentMenuProps) => { }: DeploymentMenuProps) => {
const client = useGQLClient(); const client = useGQLClient();
const { toast, dismiss } = useToast();
const [changeToProduction, setChangeToProduction] = useState(false); const [changeToProduction, setChangeToProduction] = useState(false);
const [redeployToProduction, setRedeployToProduction] = useState(false); const [redeployToProduction, setRedeployToProduction] = useState(false);
@@ -53,19 +51,9 @@ export const DeploymentMenu = ({
const isUpdated = await client.updateDeploymentToProd(deployment.id); const isUpdated = await client.updateDeploymentToProd(deployment.id);
if (isUpdated) { if (isUpdated) {
await onUpdate(); await onUpdate();
toast({ toast.success('Deployment changed to production');
id: 'deployment_changed_to_production',
title: 'Deployment changed to production',
variant: 'success',
onDismiss: dismiss,
});
} else { } else {
toast({ toast.error('Unable to change deployment to production');
id: 'deployment_not_changed_to_production',
title: 'Error changing deployment to production',
variant: 'error',
onDismiss: dismiss,
});
} }
}; };
@@ -73,19 +61,9 @@ export const DeploymentMenu = ({
const isRedeployed = await client.redeployToProd(deployment.id); const isRedeployed = await client.redeployToProd(deployment.id);
if (isRedeployed) { if (isRedeployed) {
await onUpdate(); await onUpdate();
toast({ toast.success('Redeployed to production');
id: 'redeployed_to_production',
title: 'Redeployed to production',
variant: 'success',
onDismiss: dismiss,
});
} else { } else {
toast({ toast.error('Unable to redeploy to production');
id: 'redeployed_to_production_failed',
title: 'Error redeploying to production',
variant: 'error',
onDismiss: dismiss,
});
} }
}; };
@@ -96,19 +74,9 @@ export const DeploymentMenu = ({
); );
if (isRollbacked) { if (isRollbacked) {
await onUpdate(); await onUpdate();
toast({ toast.success('Deployment rolled back');
id: 'deployment_rolled_back',
title: 'Deployment rolled back',
variant: 'success',
onDismiss: dismiss,
});
} else { } else {
toast({ toast.error('Unable to rollback deployment');
id: 'deployment_rollback_failed',
title: 'Error rolling back deployment',
variant: 'error',
onDismiss: dismiss,
});
} }
}; };
@@ -116,19 +84,9 @@ export const DeploymentMenu = ({
const isDeleted = await client.deleteDeployment(deployment.id); const isDeleted = await client.deleteDeployment(deployment.id);
if (isDeleted) { if (isDeleted) {
await onUpdate(); await onUpdate();
toast({ toast.success('Deleted deployment');
id: 'deployment_deleted',
title: 'Deployment deleted',
variant: 'success',
onDismiss: dismiss,
});
} else { } else {
toast({ toast.error('Unable to delete deployment');
id: 'deployment_not_deleted',
title: 'Error deleting deployment',
variant: 'error',
onDismiss: dismiss,
});
} }
}; };
@@ -20,7 +20,6 @@ import { cn } from 'utils/classnames';
import { useMediaQuery } from 'usehooks-ts'; import { useMediaQuery } from 'usehooks-ts';
import { SIDEBAR_MENU } from './constants'; import { SIDEBAR_MENU } from './constants';
import { UserSelect } from 'components/shared/UserSelect'; import { UserSelect } from 'components/shared/UserSelect';
import { baseUrl } from 'utils/constants';
interface SidebarProps { interface SidebarProps {
mobileOpen?: boolean; mobileOpen?: boolean;
@@ -84,7 +83,7 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
}, [orgSlug]); }, [orgSlug]);
const handleLogOut = useCallback(async () => { const handleLogOut = useCallback(async () => {
await fetch(`${baseUrl}/auth/logout`, { await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/logout`, {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
}); });
@@ -1,117 +0,0 @@
import { useEffect, useRef } from 'react';
import { Input } from '../Input';
export interface VerifyCodeInputProps {
code: string;
setCode: (code: string) => void;
submitCode: () => void;
loading: boolean;
}
export const VerifyCodeInput = ({
code,
setCode,
submitCode,
loading,
}: VerifyCodeInputProps) => {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const handlePaste = (
e: React.ClipboardEvent<HTMLInputElement>,
i: number,
) => {
e.preventDefault();
const pasteData = e.clipboardData.getData('text').replace(/\D/g, ''); // Only digits
if (pasteData.length > 0) {
let newCodeArray = code.split('');
for (let j = 0; j < pasteData.length && i + j < 6; j++) {
newCodeArray[i + j] = pasteData[j];
}
const newCode = newCodeArray.join('');
setCode(newCode);
const nextIndex = Math.min(i + pasteData.length, 5);
const nextInput = inputRefs.current[nextIndex];
if (nextInput) nextInput.focus();
if (!newCode.includes(' ')) {
submitCode();
}
}
};
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>,
i: number,
) => {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
if (e.key === 'Backspace') {
e.preventDefault();
const isEmpty = code[i] === ' ';
const newCode = !isEmpty
? code.slice(0, i) + ' ' + code.slice(i + 1, 6)
: code.slice(0, i - 1) + ' ' + code.slice(i, 6);
setCode(newCode.slice(0, 6));
if (i === 0 || !isEmpty) return;
const prev = inputRefs.current[i - 1];
if (prev) prev.focus();
return;
}
if (!e.key.match(/[0-9]/)) return;
e.preventDefault(); // Prevent the default event to avoid duplicate input
const newCode = code.slice(0, i) + e.key + code.slice(i + 1, 6);
setCode(newCode);
if (i === 5) {
submitCode();
return;
}
const next = inputRefs.current[i + 1];
if (next) next.focus();
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, i: number) => {
const value = e.target.value.slice(-1);
if (!value.match(/[0-9]/)) return;
const newCode = code.slice(0, i) + value + code.slice(i + 1, 6);
setCode(newCode);
if (i < 5) {
const next = inputRefs.current[i + 1];
if (next) next.focus();
}
if (!newCode.includes(' ')) {
submitCode();
}
};
useEffect(() => {
if (inputRefs.current[0]) {
inputRefs.current[0].focus();
}
}, []);
return (
<div className="flex gap-2">
{code.split('').map((char, i) => (
<Input
key={i}
value={char === ' ' ? '' : char}
ref={(el) => (inputRefs.current[i] = el)}
onChange={(e) => handleChange(e, i)}
onPaste={(e) => handlePaste(e, i)}
onKeyDown={(e) => handleKeyDown(e, i)}
disabled={!!loading}
style={{ textAlign: 'center' }} // Add this line to center text
/>
))}
</div>
);
};
@@ -1 +0,0 @@
export { VerifyCodeInput } from './VerifyCodeInput';
@@ -1,11 +1,9 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { VITE_WALLET_CONNECT_ID } from 'utils/constants';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
if (!VITE_WALLET_CONNECT_ID) { if (!import.meta.env.VITE_WALLET_CONNECT_ID) {
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set'); throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
} }
+2 -6
View File
@@ -14,17 +14,13 @@ import { GQLClientProvider } from './context/GQLClientContext';
import { SERVER_GQL_PATH } from './constants'; import { SERVER_GQL_PATH } from './constants';
import { Toaster } from 'components/shared/Toast'; import { Toaster } from 'components/shared/Toast';
import { LogErrorBoundary } from 'utils/log-error'; import { LogErrorBoundary } from 'utils/log-error';
import { baseUrl } from 'utils/constants';
// @ts-ignore
console.log(`v-${__VERSION__}`);
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement, document.getElementById('root') as HTMLElement,
); );
assert(baseUrl, 'VITE_SERVER_URL is not set in env'); assert(import.meta.env.VITE_SERVER_URL, 'VITE_SERVER_URL is not set in env');
const gqlEndpoint = `${baseUrl}/${SERVER_GQL_PATH}`; const gqlEndpoint = `${import.meta.env.VITE_SERVER_URL}/${SERVER_GQL_PATH}`;
const gqlClient = new GQLClient({ gqlEndpoint }); const gqlClient = new GQLClient({ gqlEndpoint });
@@ -1,99 +0,0 @@
import React, { useState } from 'react';
import { Button } from 'components/shared/Button';
import {
ArrowRightCircleFilledIcon,
LoaderIcon,
} from 'components/shared/CustomIcon';
import { WavyBorder } from 'components/shared/WavyBorder';
import { VerifyCodeInput } from 'components/shared/VerifyCodeInput';
import { verifyAccessCode } from 'utils/accessCode';
type AccessMethod = 'accesscode' | 'passkey';
type Err = { type: AccessMethod; message: string };
type AccessCodeProps = {
onCorrectAccessCode: () => void;
};
export const AccessCode: React.FC<AccessCodeProps> = ({
onCorrectAccessCode,
}) => {
const [accessCode, setAccessCode] = useState(' ');
const [error, setError] = useState<Err | null>();
const [accessMethod, setAccessMethod] = useState<AccessMethod | false>(false);
async function validateAccessCode() {
setAccessMethod('accesscode');
try {
const isValidAccessCode = await verifyAccessCode(accessCode);
// add a pause for ux
await new Promise((resolve) => setTimeout(resolve, 250));
if (isValidAccessCode) {
localStorage.setItem('accessCode', accessCode);
onCorrectAccessCode();
} else {
setError({
type: 'accesscode',
message: 'Invalid access code',
});
}
} catch (err: any) {
setError({ type: 'accesscode', message: err.message });
}
}
const loading = accessMethod;
const isValidAccessCodeLength = accessCode.trim().length === 5;
return (
<div>
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-tight">
Access Code
</div>
</div>
<WavyBorder className="self-stretch" variant="stroke" />
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
<div className="self-stretch flex-col gap-8 flex">
<div className="flex-col justify-start items-start gap-2 inline-flex">
<VerifyCodeInput
loading={!!loading}
code={accessCode}
setCode={setAccessCode}
submitCode={validateAccessCode}
/>
</div>
<Button
rightIcon={
loading && loading === 'accesscode' ? (
<LoaderIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon height="16" />
)
}
onClick={validateAccessCode}
variant={'secondary'}
disabled={!accessCode || !isValidAccessCodeLength || !!loading}
>
Submit
</Button>
{error && error.type === 'accesscode' && (
<div className="flex flex-col gap-3">
<div className="justify-center items-center gap-2 inline-flex">
<div className="text-red-500 text-sm">
Error: {error.message}.{' '}
<a href="/signup" className="underline">
Try again?
</a>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
};
@@ -1,20 +0,0 @@
import React, { useState } from 'react';
import { AccessCode } from './AccessCode';
import { SignUp } from './SignUp';
type AccessSignUpProps = {
onDone: () => void;
};
export const AccessSignUp: React.FC<AccessSignUpProps> = ({ onDone }) => {
const [isValidAccessCode, setIsValidAccessCode] = useState<boolean>(
!!localStorage.getItem('accessCode'),
);
return isValidAccessCode ? (
<SignUp onDone={onDone} />
) : (
<AccessCode onCorrectAccessCode={() => setIsValidAccessCode(true)} />
);
};
@@ -47,9 +47,7 @@ export const Login = ({ onDone }: Props) => {
console.log('Handling google redirect back'); console.log('Handling google redirect back');
try { try {
await google.handleOAuthRedirectBack(); await google.handleOAuthRedirectBack();
// @ts-ignore
wallet = await google.getEthersWallet(); wallet = await google.getEthersWallet();
// @ts-ignore
const result = await signInWithEthereum(1, 'login', wallet); const result = await signInWithEthereum(1, 'login', wallet);
if (result.error) { if (result.error) {
setError(result.error); setError(result.error);
@@ -21,7 +21,6 @@ import {
turnkeySignin, turnkeySignin,
turnkeySignup, turnkeySignup,
} from 'utils/turnkey-frontend'; } from 'utils/turnkey-frontend';
import { verifyAccessCode } from 'utils/accessCode';
type Provider = 'google' | 'github' | 'apple' | 'email'; type Provider = 'google' | 'github' | 'apple' | 'email';
@@ -46,9 +45,7 @@ export const SignUp = ({ onDone }: Props) => {
setProvider('google'); setProvider('google');
try { try {
await google.handleOAuthRedirectBack(); await google.handleOAuthRedirectBack();
// @ts-ignore
wallet = await google.getEthersWallet(); wallet = await google.getEthersWallet();
// @ts-ignore
const result = await signInWithEthereum(1, 'signup', wallet); const result = await signInWithEthereum(1, 'signup', wallet);
if (result.error) { if (result.error) {
setError({ type: 'provider', message: result.error }); setError({ type: 'provider', message: result.error });
@@ -112,29 +109,6 @@ export const SignUp = ({ onDone }: Props) => {
const loading = provider; const loading = provider;
const emailValid = /.@./.test(email); const emailValid = /.@./.test(email);
useEffect(() => {
const validateAccessCode = async () => {
const accessCode = localStorage.getItem('accessCode');
if (!accessCode) {
redirectToSignup();
return;
}
try {
await verifyAccessCode(accessCode);
} catch (err: any) {
redirectToSignup();
}
};
const redirectToSignup = () => {
localStorage.removeItem('accessCode');
window.location.href = '/signup';
};
validateAccessCode();
}, []);
return ( return (
<div> <div>
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex"> <div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
@@ -1,15 +1,14 @@
import React, { useEffect, useState } from 'react';
import { snowball } from 'utils/use-snowball'; import { snowball } from 'utils/use-snowball';
import { Login } from './Login'; import { Login } from './Login';
import { SignUp } from './SignUp';
import { useEffect, useState } from 'react';
import { Done } from './Done'; import { Done } from './Done';
import { AccessSignUp } from './AccessSignUp';
type Screen = 'login' | 'signup' | 'success'; type Screen = 'login' | 'signup' | 'success';
const DASHBOARD_URL = '/'; const DASHBOARD_URL = '/';
export const SnowballAuth: React.FC = () => { export const SnowballAuth = () => {
const path = window.location.pathname; const path = window.location.pathname;
const [screen, setScreen] = useState<Screen>( const [screen, setScreen] = useState<Screen>(
path === '/login' ? 'login' : 'signup', path === '/login' ? 'login' : 'signup',
@@ -31,7 +30,7 @@ export const SnowballAuth: React.FC = () => {
if (screen === 'signup') { if (screen === 'signup') {
return ( return (
<AccessSignUp <SignUp
onDone={() => { onDone={() => {
setScreen('success'); setScreen('success');
}} }}
@@ -6,13 +6,9 @@ const meta: Meta<typeof ChevronDownSmallIcon> = {
title: 'Icons/ChevronDownSmallIcon', title: 'Icons/ChevronDownSmallIcon',
component: ChevronDownSmallIcon, component: ChevronDownSmallIcon,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { args: {
size: { size: 'string | number' as unknown as any,
control: 'text', name: 'string',
},
name: {
control: 'text',
},
}, },
}; };
@@ -6,13 +6,9 @@ const meta: Meta<typeof ChevronRight> = {
title: 'Icons/ChevronRight', title: 'Icons/ChevronRight',
component: ChevronRight, component: ChevronRight,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { args: {
size: { size: 'string | number' as unknown as any,
control: 'text', name: 'string',
},
name: {
control: 'text',
},
}, },
}; };
@@ -6,13 +6,9 @@ const meta: Meta<typeof ChevronUpDown> = {
title: 'Icons/ChevronUpDown', title: 'Icons/ChevronUpDown',
component: ChevronUpDown, component: ChevronUpDown,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { args: {
size: { size: 'string | number' as unknown as any,
control: 'text', name: 'string',
},
name: {
control: 'text',
},
}, },
}; };
@@ -3,16 +3,12 @@ import { Meta, StoryObj } from '@storybook/react';
import { ChevronUpSmallIcon } from 'components/shared/CustomIcon'; import { ChevronUpSmallIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof ChevronUpSmallIcon> = { const meta: Meta<typeof ChevronUpSmallIcon> = {
title: 'Icons/ChevronUpSmallIcon', title: 'Components/ChevronUpSmallIcon',
component: ChevronUpSmallIcon, component: ChevronUpSmallIcon,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { args: {
size: { size: 'string | number' as unknown as any,
control: 'text', name: 'string',
},
name: {
control: 'text',
},
}, },
}; };
@@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
import { CollaboratorsIcon } from 'components/shared/CustomIcon'; import { CollaboratorsIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof CollaboratorsIcon> = { const meta: Meta<typeof CollaboratorsIcon> = {
title: 'Icons/CollaboratorsIcon', title: 'Components/CollaboratorsIcon',
component: CollaboratorsIcon, component: CollaboratorsIcon,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
-28
View File
@@ -1,28 +0,0 @@
import { baseUrl } from './constants';
export async function verifyAccessCode(
accesscode: string,
): Promise<boolean | null> {
const res = await fetch(`${baseUrl}/auth/accesscode`, {
method: 'POST',
body: JSON.stringify({
accesscode,
}),
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
});
// If API returns a non-empty 200, this is a valid access code.
if (res.status == 200) {
const isValid = (await res.json()).isValid as boolean;
console.log('isValid', isValid);
return isValid;
} else if (res.status === 204) {
return null;
} else {
throw new Error(
`Unexpected response from access code endpoint: ${res.status}: ${await res.text()}`,
);
}
}
-11
View File
@@ -1,11 +0,0 @@
export const baseUrl = import.meta.env.VITE_SERVER_URL;
export const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
export const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
export const VITE_GITHUB_PWA_TEMPLATE_REPO = import.meta.env
.VITE_GITHUB_PWA_TEMPLATE_REPO;
export const VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = import.meta.env
.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO;
export const VITE_GITHUB_CLIENT_ID = import.meta.env.VITE_GITHUB_CLIENT_ID;
export const VITE_WALLET_CONNECT_ID = import.meta.env.VITE_WALLET_CONNECT_ID;
export const VITE_BUGSNAG_API_KEY = import.meta.env.VITE_BUGSNAG_API_KEY;
export const VITE_LIT_RELAY_API_KEY = import.meta.env.VITE_LIT_RELAY_API_KEY;
+8 -8
View File
@@ -1,21 +1,21 @@
import React from 'react';
import Bugsnag from '@bugsnag/js'; import Bugsnag from '@bugsnag/js';
import BugsnagPluginReact from '@bugsnag/plugin-react'; import BugsnagPluginReact from '@bugsnag/plugin-react';
import BugsnagPerformance from '@bugsnag/browser-performance'; import BugsnagPerformance from '@bugsnag/browser-performance';
import React from 'react';
import { VITE_BUGSNAG_API_KEY } from './constants'; const bugsnagApiKey = import.meta.env.VITE_BUGSNAG_API_KEY;
if (VITE_BUGSNAG_API_KEY) { if (bugsnagApiKey) {
Bugsnag.start({ Bugsnag.start({
apiKey: VITE_BUGSNAG_API_KEY, apiKey: bugsnagApiKey,
plugins: [new BugsnagPluginReact()], plugins: [new BugsnagPluginReact()],
}); });
BugsnagPerformance.start({ apiKey: VITE_BUGSNAG_API_KEY }); BugsnagPerformance.start({ apiKey: bugsnagApiKey });
} }
export const errorLoggingEnabled = !!VITE_BUGSNAG_API_KEY; export const errorLoggingEnabled = !!bugsnagApiKey;
export const LogErrorBoundary = VITE_BUGSNAG_API_KEY export const LogErrorBoundary = bugsnagApiKey
? Bugsnag.getPlugin('react')!.createErrorBoundary(React) ? Bugsnag.getPlugin('react')!.createErrorBoundary(React)
: ({ children }: any) => children; : ({ children }: any) => children;
@@ -28,7 +28,7 @@ export function logError(error: Error) {
} }
console.error(...errors); console.error(...errors);
if (VITE_BUGSNAG_API_KEY) { if (import.meta.env.VITE_BUGSNAG_API_KEY) {
Bugsnag.notify(error); Bugsnag.notify(error);
} }
} }
+1 -3
View File
@@ -2,8 +2,6 @@ import { SiweMessage } from 'siwe';
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers'; import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { baseUrl } from './constants';
const domain = window.location.host; const domain = window.location.host;
const origin = window.location.origin; const origin = window.location.origin;
@@ -19,7 +17,7 @@ export async function signInWithEthereum(
); );
const signature = await wallet.signMessage(message); const signature = await wallet.signMessage(message);
const res = await fetch(`${baseUrl}/auth/validate`, { const res = await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/validate`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -1,7 +1,10 @@
import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http'; import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http';
import { WebauthnStamper } from '@turnkey/webauthn-stamper'; import { WebauthnStamper } from '@turnkey/webauthn-stamper';
import { baseUrl, PASSKEY_WALLET_RPID, TURNKEY_BASE_URL } from './constants'; const baseUrl = import.meta.env.VITE_SERVER_URL;
const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
// All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms // All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
// We only support ES256, which is listed here // We only support ES256, which is listed here
+3 -4
View File
@@ -5,17 +5,16 @@ import {
LitGoogleAuth, LitGoogleAuth,
LitPasskeyAuth, LitPasskeyAuth,
} from '@snowballtools/auth-lit'; } from '@snowballtools/auth-lit';
import { VITE_LIT_RELAY_API_KEY } from './constants';
export const snowball = Snowball.withAuth({ export const snowball = Snowball.withAuth({
google: LitGoogleAuth.configure({ google: LitGoogleAuth.configure({
litRelayApiKey: VITE_LIT_RELAY_API_KEY!, litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
}), }),
// apple: LitAppleAuth.configure({ // apple: LitAppleAuth.configure({
// litRelayApiKey: VITE_LIT_RELAY_API_KEY!, // litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
// }), // }),
passkey: LitPasskeyAuth.configure({ passkey: LitPasskeyAuth.configure({
litRelayApiKey: VITE_LIT_RELAY_API_KEY!, litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
}), }),
}).create({ }).create({
initialChain: SnowballChain.sepolia, initialChain: SnowballChain.sepolia,
-5
View File
@@ -1,7 +1,5 @@
import { defineConfig, PluginOption } from 'vite'; import { defineConfig, PluginOption } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { exec } from 'child_process';
import { promisify } from 'util';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
@@ -18,8 +16,5 @@ export default defineConfig({
}, },
define: { define: {
'process.env': {}, 'process.env': {},
__VERSION__: JSON.stringify(
(await promisify(exec)('git rev-parse --short HEAD')).stdout.trim(),
),
}, },
}); });
File diff suppressed because one or more lines are too long
+1 -4
View File
@@ -20,10 +20,7 @@ var __async = (__this, __arguments, generator) => {
}; };
// src/client.ts // src/client.ts
import { import { ApolloClient, InMemoryCache } from "@apollo/client";
ApolloClient,
InMemoryCache
} from "@apollo/client";
// src/queries.ts // src/queries.ts
import { gql } from "@apollo/client"; import { gql } from "@apollo/client";
File diff suppressed because one or more lines are too long
+82 -139
View File
@@ -1,13 +1,8 @@
import { import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
ApolloClient,
DefaultOptions,
InMemoryCache,
NormalizedCacheObject,
} from "@apollo/client";
import * as queries from "./queries"; import * as queries from './queries';
import * as types from "./types"; import * as types from './types';
import * as mutations from "./mutations"; import * as mutations from './mutations';
export interface GraphQLConfig { export interface GraphQLConfig {
gqlEndpoint: string; gqlEndpoint: string;
@@ -16,13 +11,13 @@ export interface GraphQLConfig {
// TODO: check options // TODO: check options
const defaultOptions: DefaultOptions = { const defaultOptions: DefaultOptions = {
watchQuery: { watchQuery: {
fetchPolicy: "no-cache", fetchPolicy: 'no-cache',
errorPolicy: "ignore", errorPolicy: 'ignore'
}, },
query: { query: {
fetchPolicy: "no-cache", fetchPolicy: 'no-cache',
errorPolicy: "all", errorPolicy: 'all'
}, }
}; };
export class GQLClient { export class GQLClient {
@@ -33,13 +28,13 @@ export class GQLClient {
uri: config.gqlEndpoint, uri: config.gqlEndpoint,
cache: new InMemoryCache(), cache: new InMemoryCache(),
defaultOptions, defaultOptions,
credentials: "include", credentials: 'include'
}); });
} }
async getUser () : Promise<types.GetUserResponse> { async getUser () : Promise<types.GetUserResponse> {
const { data } = await this.client.query({ const { data } = await this.client.query({
query: queries.getUser, query: queries.getUser
}); });
return data; return data;
@@ -49,21 +44,19 @@ export class GQLClient {
const { data } = await this.client.query({ const { data } = await this.client.query({
query: queries.getProject, query: queries.getProject,
variables: { variables: {
projectId, projectId
}, }
}); });
return data; return data;
} }
async getProjectsInOrganization( async getProjectsInOrganization (organizationSlug: string) : Promise<types.GetProjectsInOrganizationResponse> {
organizationSlug: string
): Promise<types.GetProjectsInOrganizationResponse> {
const { data } = await this.client.query({ const { data } = await this.client.query({
query: queries.getProjectsInOrganization, query: queries.getProjectsInOrganization,
variables: { variables: {
organizationSlug, organizationSlug
}, }
}); });
return data; return data;
@@ -71,216 +64,179 @@ export class GQLClient {
async getOrganizations () : Promise<types.GetOrganizationsResponse> { async getOrganizations () : Promise<types.GetOrganizationsResponse> {
const { data } = await this.client.query({ const { data } = await this.client.query({
query: queries.getOrganizations, query: queries.getOrganizations
}); });
return data; return data;
} }
async getDeployments( async getDeployments (projectId: string) : Promise<types.GetDeploymentsResponse> {
projectId: string
): Promise<types.GetDeploymentsResponse> {
const { data } = await this.client.query({ const { data } = await this.client.query({
query: queries.getDeployments, query: queries.getDeployments,
variables: { variables: {
projectId, projectId
}, }
}); });
return data; return data;
} }
async getEnvironmentVariables( async getEnvironmentVariables (projectId: string) : Promise<types.GetEnvironmentVariablesResponse> {
projectId: string
): Promise<types.GetEnvironmentVariablesResponse> {
const { data } = await this.client.query({ const { data } = await this.client.query({
query: queries.getEnvironmentVariables, query: queries.getEnvironmentVariables,
variables: { variables: {
projectId, projectId
}, }
}); });
return data; return data;
} }
async getProjectMembers( async getProjectMembers (projectId: string) : Promise<types.GetProjectMembersResponse> {
projectId: string
): Promise<types.GetProjectMembersResponse> {
const result = await this.client.query({ const result = await this.client.query({
query: queries.getProjectMembers, query: queries.getProjectMembers,
variables: { variables: {
projectId, projectId
}, }
}); });
return result.data; return result.data;
} }
async addProjectMember( async addProjectMember (projectId: string, data: types.AddProjectMemberInput) : Promise<types.AddProjectMemberResponse> {
projectId: string,
data: types.AddProjectMemberInput
): Promise<types.AddProjectMemberResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.addProjectMember, mutation: mutations.addProjectMember,
variables: { variables: {
projectId, projectId,
data, data
}, }
}); });
return result.data; return result.data;
} }
async updateProjectMember( async updateProjectMember (projectMemberId: string, data: types.UpdateProjectMemberInput): Promise<types.UpdateProjectMemberResponse> {
projectMemberId: string,
data: types.UpdateProjectMemberInput
): Promise<types.UpdateProjectMemberResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.updateProjectMember, mutation: mutations.updateProjectMember,
variables: { variables: {
projectMemberId, projectMemberId,
data, data
}, }
}); });
return result.data; return result.data;
} }
async removeProjectMember( async removeProjectMember (projectMemberId: string): Promise<types.RemoveProjectMemberResponse> {
projectMemberId: string
): Promise<types.RemoveProjectMemberResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.removeProjectMember, mutation: mutations.removeProjectMember,
variables: { variables: {
projectMemberId, projectMemberId
}, }
}); });
return result.data; return result.data;
} }
async searchProjects( async searchProjects (searchText: string) : Promise<types.SearchProjectsResponse> {
searchText: string
): Promise<types.SearchProjectsResponse> {
const { data } = await this.client.query({ const { data } = await this.client.query({
query: queries.searchProjects, query: queries.searchProjects,
variables: { variables: {
searchText, searchText
}, }
}); });
return data; return data;
} }
async addEnvironmentVariables( async addEnvironmentVariables (projectId: string, data: types.AddEnvironmentVariableInput[]): Promise<types.AddEnvironmentVariablesResponse> {
projectId: string,
data: types.AddEnvironmentVariableInput[]
): Promise<types.AddEnvironmentVariablesResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.addEnvironmentVariables, mutation: mutations.addEnvironmentVariables,
variables: { variables: {
projectId, projectId,
data, data
}, }
}); });
return result.data; return result.data;
} }
async updateEnvironmentVariable( async updateEnvironmentVariable (environmentVariableId: string, data: types.UpdateEnvironmentVariableInput): Promise<types.UpdateEnvironmentVariableResponse> {
environmentVariableId: string,
data: types.UpdateEnvironmentVariableInput
): Promise<types.UpdateEnvironmentVariableResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.updateEnvironmentVariable, mutation: mutations.updateEnvironmentVariable,
variables: { variables: {
environmentVariableId, environmentVariableId,
data, data
}, }
}); });
return result.data; return result.data;
} }
async removeEnvironmentVariable( async removeEnvironmentVariable (environmentVariableId: string): Promise<types.RemoveEnvironmentVariableResponse> {
environmentVariableId: string
): Promise<types.RemoveEnvironmentVariableResponse> {
const { data } = await this.client.mutate({ const { data } = await this.client.mutate({
mutation: mutations.removeEnvironmentVariable, mutation: mutations.removeEnvironmentVariable,
variables: { variables: {
environmentVariableId, environmentVariableId
}, }
}); });
return data; return data;
} }
async updateDeploymentToProd( async updateDeploymentToProd (deploymentId: string): Promise<types.UpdateDeploymentToProdResponse> {
deploymentId: string
): Promise<types.UpdateDeploymentToProdResponse> {
const { data } = await this.client.mutate({ const { data } = await this.client.mutate({
mutation: mutations.updateDeploymentToProd, mutation: mutations.updateDeploymentToProd,
variables: { variables: {
deploymentId, deploymentId
}, }
}); });
return data; return data;
} }
async addProject( async addProject (organizationSlug: string, data: types.AddProjectInput): Promise<types.AddProjectResponse> {
organizationSlug: string,
data: types.AddProjectInput
): Promise<types.AddProjectResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.addProject, mutation: mutations.addProject,
variables: { variables: {
organizationSlug, organizationSlug,
data, data
}, }
}); });
return result.data; return result.data;
} }
async updateProject( async updateProject (projectId: string, data: types.UpdateProjectInput): Promise<types.UpdateProjectResponse> {
projectId: string,
data: types.UpdateProjectInput
): Promise<types.UpdateProjectResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.updateProjectMutation, mutation: mutations.updateProjectMutation,
variables: { variables: {
projectId, projectId,
data, data
}, }
}); });
return result.data; return result.data;
} }
async updateDomain( async updateDomain (domainId: string, data: types.UpdateDomainInput): Promise<types.UpdateDomainResponse> {
domainId: string,
data: types.UpdateDomainInput
): Promise<types.UpdateDomainResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.updateDomainMutation, mutation: mutations.updateDomainMutation,
variables: { variables: {
domainId, domainId,
data, data
}, }
}); });
return result.data; return result.data;
} }
async redeployToProd( async redeployToProd (deploymentId: string): Promise<types.RedeployToProdResponse> {
deploymentId: string
): Promise<types.RedeployToProdResponse> {
const { data } = await this.client.mutate({ const { data } = await this.client.mutate({
mutation: mutations.redeployToProd, mutation: mutations.redeployToProd,
variables: { variables: {
deploymentId, deploymentId
}, }
}); });
return data; return data;
@@ -290,8 +246,8 @@ export class GQLClient {
const { data } = await this.client.mutate({ const { data } = await this.client.mutate({
mutation: mutations.deleteProject, mutation: mutations.deleteProject,
variables: { variables: {
projectId, projectId
}, }
}); });
return data; return data;
@@ -301,79 +257,66 @@ export class GQLClient {
const { data } = await this.client.mutate({ const { data } = await this.client.mutate({
mutation: mutations.deleteDomain, mutation: mutations.deleteDomain,
variables: { variables: {
domainId, domainId
}, }
}); });
return data; return data;
} }
async rollbackDeployment( async rollbackDeployment (projectId: string, deploymentId: string): Promise<types.RollbackDeploymentResponse> {
projectId: string,
deploymentId: string
): Promise<types.RollbackDeploymentResponse> {
const { data } = await this.client.mutate({ const { data } = await this.client.mutate({
mutation: mutations.rollbackDeployment, mutation: mutations.rollbackDeployment,
variables: { variables: {
projectId, projectId,
deploymentId, deploymentId
}, }
}); });
return data; return data;
} }
async deleteDeployment( async deleteDeployment (deploymentId: string): Promise<types.DeleteDeploymentResponse> {
deploymentId: string
): Promise<types.DeleteDeploymentResponse> {
const { data } = await this.client.mutate({ const { data } = await this.client.mutate({
mutation: mutations.deleteDeployment, mutation: mutations.deleteDeployment,
variables: { variables: {
deploymentId, deploymentId
}, }
}); });
return data; return data;
} }
async addDomain( async addDomain (projectId: string, data: types.AddDomainInput): Promise<types.AddDomainResponse> {
projectId: string,
data: types.AddDomainInput
): Promise<types.AddDomainResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.addDomain, mutation: mutations.addDomain,
variables: { variables: {
projectId, projectId,
data, data
}, }
}); });
return result.data; return result.data;
} }
async getDomains( async getDomains (projectId: string, filter?: types.FilterDomainInput): Promise<types.GetDomainsResponse> {
projectId: string,
filter?: types.FilterDomainInput
): Promise<types.GetDomainsResponse> {
const { data } = await this.client.query({ const { data } = await this.client.query({
query: queries.getDomains, query: queries.getDomains,
variables: { variables: {
projectId, projectId,
filter, filter
}, }
}); });
return data; return data;
} }
async authenticateGitHub( async authenticateGitHub (code: string): Promise<types.AuthenticateGitHubResponse> {
code: string
): Promise<types.AuthenticateGitHubResponse> {
const { data } = await this.client.mutate({ const { data } = await this.client.mutate({
mutation: mutations.authenticateGitHub, mutation: mutations.authenticateGitHub,
variables: { variables: {
code, code
}, }
}); });
return data; return data;
@@ -381,7 +324,7 @@ export class GQLClient {
async unauthenticateGithub (): Promise<types.UnauthenticateGitHubResponse> { async unauthenticateGithub (): Promise<types.UnauthenticateGitHubResponse> {
const { data } = await this.client.mutate({ const { data } = await this.client.mutate({
mutation: mutations.unauthenticateGitHub, mutation: mutations.unauthenticateGitHub
}); });
return data; return data;
-5
View File
@@ -10575,11 +10575,6 @@ exponential-backoff@^3.1.1:
resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6"
integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==
express-async-errors@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/express-async-errors/-/express-async-errors-3.1.1.tgz#6053236d61d21ddef4892d6bd1d736889fc9da41"
integrity sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==
express-session@^1.18.0: express-session@^1.18.0:
version "1.18.0" version "1.18.0"
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.0.tgz#a6ae39d9091f2efba5f20fc5c65a3ce7c9ce16a3" resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.0.tgz#a6ae39d9091f2efba5f20fc5c65a3ce7c9ce16a3"