Compare commits

...
Author SHA1 Message Date
Vivian Phung 8038744dc1 feat: access code 2024-06-22 01:08:54 +00:00
Vivian Phung 934aa1a26b Refactor: Env to utils/constants (#210)
This PR centralizes all the environment variable references into a single constants file. The change includes replacing various `import.meta.env` references with imports from the new `utils/constants` module. This improves maintainability by providing a single place to manage environment variables.
2024-06-21 21:07:41 -04:00
Gilbert d975390a1b Fix url env var 2024-06-20 14:54:17 -05:00
Gilbert f77323364c Bump 2024-06-20 04:06:16 -05:00
Gilbert c6a78f2116 Just send back the message, this is a demo after all 2024-06-20 04:05:48 -05:00
Vivian Phung cff9a5b2ea [nit] DeploymentMenu dependencies cleanup (#204)
### TL;DR

This PR updates the project settings.

### What changed?

The project settings have been refactored for better organization and readability.

### How to test?

To test this change, navigate to the project settings and ensure all options are functioning as expected.

### Why make this change?

This change was made to improve the user experience when navigating through the project settings.

---
2024-06-20 00:40:32 -04:00
Vivian Phung 003b83ba21 Refactor: DeploymentMenu uses toast component (#203)
### TL;DR

This PR includes updates to the project settings.

### What changed?

The project settings have been refactored for better usability and consistency with other components.

### How to test?

To test this change, navigate to the project settings and ensure all options are working as expected.

### Why make this change?

This change was made to improve the user experience and maintain consistency across the application.

---
2024-06-20 00:36:43 -04:00
Vivian Phung 82a1c151a8 Refactor: AssignDomainDialog uses Modal component (#202)
### TL;DR

This PR includes updates to the project settings.

### What changed?

The project settings have been refactored for better organization and readability.

### How to test?

To test this change, navigate to the project settings and ensure all options are functioning as expected.

### Why make this change?

This change was made to improve the user experience when navigating through the project settings.

---
2024-06-20 00:32:41 -04:00
Gilbert 2ada11f311 Support async express handlers 2024-06-19 22:59:20 -05:00
Gilbert 6e32d0678a Add generic error handling 2024-06-19 22:31:07 -05:00
Gilbert 198478f5fa Add logs for debugging 2024-06-19 14:22:55 -05:00
dboreham 552dfe783e Merge pull request #209 from snowball-tools/dboreham/add-rpid-config
Add missing rpid env var
2024-06-18 20:04:19 -06:00
Gilbert e2bf5d052c Bump server version 2024-06-18 17:46:47 -05:00
dboreham 33323b9bbf Merge pull request #208 from snowball-tools/dboreham/error-checked-deployment
Make deployment scripts more robust
2024-06-18 13:51:33 -06:00
dboreham e2a3254563 Add missing rpid env var 2024-06-18 13:47:26 -06:00
dboreham 8589bf4094 Make deployment scripts more robust 2024-06-18 11:15:31 -06:00
dboreham f8ebdfe7aa Merge pull request #207 from snowball-tools/telackey-patch-1
Update config.staging.yml
2024-06-18 06:31:58 -06:00
telackey 2166b2f800 Update config.staging.yml 2024-06-17 23:41:01 -05:00
dboreham cdd8d15e73 Increase gas for staging deployment chain profile to match prod (#205) 2024-06-17 09:49:52 +05:30
Vivian Phung dc7b251988 add version backend (temp) (#201)
* add version backend (temp)

* staging router endpoint

* remove frontend
2024-06-05 17:38:19 +01:00
Gilbert bfb4a3f30b Revert "remove email signup for now"
This reverts commit 0e9c3a07fd.
2024-06-04 16:26:15 -05:00
Gilbert 8f7fc888a9 Log commit hash 2024-06-04 15:56:52 -05:00
Vivian Phung 0e9c3a07fd remove email signup for now 2024-06-04 15:56:52 -05:00
Vivian Phung 61e3e88a6c Refactor Input and SearchBar (#199)
### TL;DR

This pull request refactors the `SearchBar` and `Input` components, adding a `ref` to the former and removing an incorrect understanding of `react-hook-form` (yes, i prev "fix" the component) in the latter.

### What changed?

A ref is added to the SearchBar component for better control and handling. In the Input component, we have eliminated the usage of 'react-hook-form' and as a result, the 'register' prop is removed. This makes the component less reliant on specific libraries and more reusable.

### How to test?

Ensure that proper testing is done on the updated components. Make sure that the `SearchBar` works as expected with its ref and that Input does not depend on 'react-hook-form' anymore.

### Why make this change?

This change was made to improve the functionality of the `SearchBar` and the flexibility of the Input component, making them more effective and reusable respectively. The changes also align with the current code quality standards and best practices.
2024-05-22 15:06:50 -04:00
Vivian Phung 7b5ba1a5d0 correct suppressRefError (#198)
### TL;DR

This PR refactors the `UserSelect` component, adjusting the call to `getToggleButtonProps`.

### What changed?

The `getToggleButtonProps` method in the `UserSelect` component now takes in two separate objects, one for the `ref` and another for `suppressRefError`, instead of a single one.

### How to test?

Verify the component functionality hasn't changed and there are no reference errors.

### Why make this change?

This code changes improve the readability and maintainability of this component by clearly separating the component reference and error suppression configurations in separate objects.
2024-05-22 15:02:51 -04:00
Vivian Phung b35f4033c5 Refactor: Collaborator Project Settings (#197)
### TL;DR

AddMemberDialog component now uses a Select dropdown for permissions instead of Checkboxes. CollaboratorsTabPanel now includes dismiss functionality for toasts.

### What changed?

- Updated AddMemberDialog to use a Select dropdown for permissions
- Added dismiss functionality for toasts in CollaboratorsTabPanel

### How to test?

Test the functionality of selecting permissions using the dropdown and toast dismissal in CollaboratorsTabPanel.

### Why make this change?

To improve user experience and UI consistency in permissions selection and toast management.
2024-05-22 14:58:47 -04:00
40 changed files with 975 additions and 509 deletions
+1
View File
@@ -19,6 +19,7 @@
"cors": "^2.8.5",
"debug": "^4.3.1",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"express-session": "^1.18.0",
"fs-extra": "^11.2.0",
"graphql": "^16.8.1",
+1
View File
@@ -1,3 +1,4 @@
import 'express-async-errors';
import 'reflect-metadata';
import debug from 'debug';
import fs from 'fs';
+57 -39
View File
@@ -17,7 +17,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
return context.user;
},
organizations: async (_:any, __: any, context: any) => {
organizations: async (_: any, __: any, context: any) => {
return service.getOrganizationsByUserId(context.user);
},
@@ -25,8 +25,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectById(projectId);
},
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
return service.getProjectsInOrganization(context.user, organizationSlug);
projectsInOrganization: async (
_: any,
{ organizationSlug }: { organizationSlug: string },
context: any,
) => {
return service.getProjectsInOrganization(
context.user,
organizationSlug,
);
},
deployments: async (_: any, { projectId }: { projectId: string }) => {
@@ -35,7 +42,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
environmentVariables: async (
_: any,
{ projectId }: { projectId: string }
{ projectId }: { projectId: string },
) => {
return service.getEnvironmentVariablesByProjectId(projectId);
},
@@ -44,7 +51,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectMembersByProjectId(projectId);
},
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
searchProjects: async (
_: any,
{ searchText }: { searchText: string },
context: any,
) => {
return service.searchProjects(context.user, searchText);
},
@@ -52,11 +63,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
filter
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
filter,
}: { projectId: string; filter?: FindOptionsWhere<Domain> },
) => {
return service.getDomainsByProjectId(projectId, filter);
}
},
},
// TODO: Return error in GQL response
@@ -64,10 +75,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
removeProjectMember: async (
_: any,
{ projectMemberId }: { projectMemberId: string },
context: any
context: any,
) => {
try {
return await service.removeProjectMember(context.user, projectMemberId);
return await service.removeProjectMember(
context.user,
projectMemberId,
);
} catch (err) {
log(err);
return false;
@@ -78,13 +92,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectMemberId,
data
data,
}: {
projectMemberId: string;
data: {
permissions: Permission[];
};
}
},
) => {
try {
return await service.updateProjectMember(projectMemberId, data);
@@ -98,14 +112,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
data
data,
}: {
projectId: string;
data: {
email: string;
permissions: Permission[];
};
}
},
) => {
try {
return Boolean(await service.addProjectMember(projectId, data));
@@ -119,15 +133,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
data
data,
}: {
projectId: string;
data: { environments: string[]; key: string; value: string }[];
}
},
) => {
try {
return Boolean(
await service.addEnvironmentVariables(projectId, data)
await service.addEnvironmentVariables(projectId, data),
);
} catch (err) {
log(err);
@@ -139,16 +153,16 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
environmentVariableId,
data
data,
}: {
environmentVariableId: string;
data: DeepPartial<EnvironmentVariable>;
}
},
) => {
try {
return await service.updateEnvironmentVariable(
environmentVariableId,
data
data,
);
} catch (err) {
log(err);
@@ -158,7 +172,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
removeEnvironmentVariable: async (
_: any,
{ environmentVariableId }: { environmentVariableId: string }
{ environmentVariableId }: { environmentVariableId: string },
) => {
try {
return await service.removeEnvironmentVariable(environmentVariableId);
@@ -171,10 +185,12 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateDeploymentToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
context: any
context: any,
) => {
try {
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
return Boolean(
await service.updateDeploymentToProd(context.user, deploymentId),
);
} catch (err) {
log(err);
return false;
@@ -185,9 +201,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
organizationSlug,
data
data,
}: { organizationSlug: string; data: DeepPartial<Project> },
context: any
context: any,
) => {
try {
return await service.addProject(context.user, organizationSlug, data);
@@ -199,7 +215,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateProject: async (
_: any,
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
{ projectId, data }: { projectId: string; data: DeepPartial<Project> },
) => {
try {
return await service.updateProject(projectId, data);
@@ -212,10 +228,12 @@ export const createResolvers = async (service: Service): Promise<any> => {
redeployToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
context: any
context: any,
) => {
try {
return Boolean(await service.redeployToProd(context.user, deploymentId));
return Boolean(
await service.redeployToProd(context.user, deploymentId),
);
} catch (err) {
log(err);
return false;
@@ -244,8 +262,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
deploymentId
}: { deploymentId: string; projectId: string }
deploymentId,
}: { deploymentId: string; projectId: string },
) => {
try {
return await service.rollbackDeployment(projectId, deploymentId);
@@ -257,9 +275,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
deleteDeployment: async (
_: any,
{
deploymentId
}: { deploymentId: string; }
{ deploymentId }: { deploymentId: string },
) => {
try {
return await service.deleteDeployment(deploymentId);
@@ -271,7 +287,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
addDomain: async (
_: any,
{ projectId, data }: { projectId: string; data: { name: string } }
{ projectId, data }: { projectId: string; data: { name: string } },
) => {
try {
return Boolean(await service.addDomain(projectId, data));
@@ -283,7 +299,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateDomain: async (
_: any,
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> },
) => {
try {
return await service.updateDomain(domainId, data);
@@ -296,7 +312,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
authenticateGitHub: async (
_: any,
{ code }: { code: string },
context: any
context: any,
) => {
try {
return await service.authenticateGitHub(code, context.user);
@@ -308,12 +324,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
unauthenticateGitHub: async (_: any, __: object, context: any) => {
try {
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
return service.unauthenticateGitHub(context.user, {
gitHubToken: null,
});
} catch (err) {
log(err);
return false;
}
}
}
},
},
};
};
+12
View File
@@ -5,6 +5,16 @@ import { authenticateUser, createUser } from '../turnkey-backend';
const router = Router();
router.post('/accesscode', async (req, res) => {
console.log('Access Code', req.body);
const { accesscode } = req.body;
if (accesscode === '444444') {
return res.send({ isValid: true });
} else {
return res.sendStatus(204);
}
});
//
// Turnkey
//
@@ -19,6 +29,7 @@ router.get('/registration/:email', async (req, res) => {
});
router.post('/register', async (req, res) => {
console.log('Register', req.body);
const { email, challenge, attestation } = req.body;
const user = await createUser(req.app.get('service'), {
challenge,
@@ -31,6 +42,7 @@ router.post('/register', async (req, res) => {
});
router.post('/authenticate', async (req, res) => {
console.log('Authenticate', req.body);
const { signedWhoamiRequest } = req.body;
const user = await authenticateUser(
req.app.get('service'),
+9
View File
@@ -0,0 +1,9 @@
import { Router } from 'express';
const router = Router();
router.get('/version', async (req, res) => {
return res.send({ version: '0.0.7' });
});
export default router;
+7
View File
@@ -17,6 +17,7 @@ import { ServerConfig } from './config';
import { DEFAULT_GQL_PATH } from './constants';
import githubRouter from './routes/github';
import authRouter from './routes/auth';
import stagingRouter from './routes/staging';
import { Service } from './service';
const log = debug('snowball:server');
@@ -109,6 +110,12 @@ export const createAndStartServer = async (
app.set('service', service);
app.use('/auth', authRouter);
app.use('/api/github', githubRouter);
app.use('/staging', stagingRouter);
app.use((err: any, req: any, res: any, next: any) => {
console.error(err);
res.status(500).json({ error: err.message });
});
httpServer.listen(port, host, () => {
log(`Server is listening on ${host}:${port}${server.graphqlPath}`);
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -3,7 +3,7 @@ services:
restEndpoint: 'http://console.laconic.com:1317'
gqlEndpoint: 'http://console.laconic.com:9473/api'
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
bondId: 098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
chainId: laconic_9000-1
gas: 550000
gas: 1200000
fees: 200000aphoton
+35 -3
View File
@@ -64,7 +64,13 @@ echo "Files generated successfully."
RECORD_FILE=records/application-record.yml
# Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
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 $RECORD_ID
@@ -73,15 +79,36 @@ REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set name: $REGISTRY_APP_CRN@${PACKAGE_VERSION}"
exit $rc
fi
sleep 2
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
# Set name if latest release
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"
# Check if record found for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
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
echo "No record found for $REGISTRY_APP_CRN."
exit 1
@@ -90,6 +117,11 @@ fi
RECORD_FILE=records/application-deployment-request.yml
sleep 2
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
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 $DEPLOYMENT_REQUEST_ID
+37 -5
View File
@@ -44,8 +44,8 @@ record:
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
@@ -70,7 +70,13 @@ echo "Files generated successfully."
RECORD_FILE=staging-records/application-record.yml
# Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
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 $RECORD_ID
@@ -79,15 +85,36 @@ REGISTRY_APP_CRN="crn://staging-snowballtools/applications/staging-snowballtools
sleep 2
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
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
# Set name if latest release
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"
# Check if record found for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
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
echo "No record found for $REGISTRY_APP_CRN."
exit 1
@@ -96,6 +123,11 @@ fi
RECORD_FILE=staging-records/application-deployment-request.yml
sleep 2
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
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 $DEPLOYMENT_REQUEST_ID
+3 -2
View File
@@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Projects from './pages/org-slug';
@@ -10,8 +11,8 @@ import ProjectSearchLayout from './layouts/ProjectSearch';
import Index from './pages';
import AuthPage from './pages/AuthPage';
import { DashboardLayout } from './pages/org-slug/layout';
import { useEffect } from 'react';
import Web3Provider from 'context/Web3Provider';
import { baseUrl } from 'utils/constants';
const router = createBrowserRouter([
{
@@ -59,7 +60,7 @@ function App() {
// Hacky way of checking session
// TODO: Handle redirect backs
useEffect(() => {
fetch(`${import.meta.env.VITE_SERVER_URL}/auth/session`, {
fetch(`${baseUrl}/auth/session`, {
credentials: 'include',
}).then((res) => {
if (res.status !== 200) {
+7 -2
View File
@@ -1,16 +1,21 @@
import {
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO,
VITE_GITHUB_PWA_TEMPLATE_REPO,
} from 'utils/constants';
export default [
{
id: '1',
name: 'Progressive Web App (PWA)',
icon: 'pwa',
repoFullName: `${import.meta.env.VITE_GITHUB_PWA_TEMPLATE_REPO}`,
repoFullName: `${VITE_GITHUB_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
id: '2',
name: 'Image Upload PWA',
icon: 'pwa',
repoFullName: `${import.meta.env.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
repoFullName: `${VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
@@ -6,7 +6,7 @@ import { Input, InputProps } from './shared/Input';
const SearchBar: React.ForwardRefRenderFunction<
HTMLInputElement,
InputProps & RefAttributes<HTMLInputElement>
> = ({ value, onChange, placeholder = 'Search', ...props }) => {
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
return (
<div className="relative flex w-full">
<Input
@@ -18,6 +18,7 @@ const SearchBar: React.ForwardRefRenderFunction<
appearance="borderless"
className="w-full lg:w-[459px]"
{...props}
ref={ref}
/>
</div>
);
@@ -14,11 +14,10 @@ import { useToast } from '../../shared/Toast';
import { IconWithFrame } from '../../shared/IconWithFrame';
import { Heading } from '../../shared/Heading';
import { MockConnectGitCard } from './MockConnectGitCard';
import { VITE_GITHUB_CLIENT_ID } from 'utils/constants';
const SCOPES = 'repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
import.meta.env.VITE_GITHUB_CLIENT_ID
}&scope=${encodeURIComponent(SCOPES)}`;
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`;
interface ConnectAccountInterface {
onAuth: (token: string) => void;
@@ -1,13 +1,8 @@
import { CopyBlock, atomOneLight } from 'react-code-blocks';
import { Link } from 'react-router-dom';
import {
Button,
Dialog,
DialogHeader,
DialogBody,
DialogFooter,
} from '@snowballtools/material-tailwind-react-fork';
import { Modal } from 'components/shared/Modal';
import { Button } from 'components/shared/Button';
interface AssignDomainProps {
open: boolean;
@@ -16,36 +11,31 @@ interface AssignDomainProps {
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
return (
<Dialog open={open} handler={handleOpen}>
<DialogHeader>Assign Domain</DialogHeader>
<DialogBody>
In order to assign a domain to your production deployments, configure it
in the{' '}
{/* TODO: Fix selection of project settings tab on navigation to domains */}
<Link to="../settings/domains" className="text-light-blue-800 inline">
project settings{' '}
</Link>
(recommended). If you want to assign to this specific deployment,
however, you can do so using our command-line interface:
{/* https://github.com/rajinwonderland/react-code-blocks/issues/138 */}
<CopyBlock
text="snowball alias <deployment> <domain>"
language=""
showLineNumbers={false}
theme={atomOneLight}
/>
</DialogBody>
<DialogFooter className="flex justify-start">
<Button
className="rounded-3xl"
variant="gradient"
color="blue"
onClick={handleOpen}
>
<span>Okay</span>
</Button>
</DialogFooter>
</Dialog>
<Modal open={open} onOpenChange={handleOpen}>
<Modal.Content>
<Modal.Header>Assign Domain</Modal.Header>
<Modal.Body>
In order to assign a domain to your production deployments, configure
it in the{' '}
{/* TODO: Fix selection of project settings tab on navigation to domains */}
<Link to="../settings/domains" className="text-light-blue-800 inline">
project settings{' '}
</Link>
(recommended). If you want to assign to this specific deployment,
however, you can do so using our command-line interface:
{/* https://github.com/rajinwonderland/react-code-blocks/issues/138 */}
<CopyBlock
text="snowball alias <deployment> <domain>"
language=""
showLineNumbers={false}
theme={atomOneLight}
/>
</Modal.Body>
<Modal.Footer className="flex justify-start">
<Button onClick={handleOpen}>Okay</Button>
</Modal.Footer>
</Modal.Content>
</Modal>
);
};
@@ -1,5 +1,12 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { useState, ComponentPropsWithRef } from 'react';
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@snowballtools/material-tailwind-react-fork';
import { Deployment, Domain, Environment, Project } from 'gql-client';
import { Button } from 'components/shared/Button';
import {
@@ -11,17 +18,11 @@ import {
UndoIcon,
CrossCircleIcon,
} from 'components/shared/CustomIcon';
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@snowballtools/material-tailwind-react-fork';
import { ComponentPropsWithRef } from 'react';
import AssignDomainDialog from './AssignDomainDialog';
import { useGQLClient } from 'context/GQLClientContext';
import { cn } from 'utils/classnames';
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
import { useToast } from 'components/shared/Toast';
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
deployment: Deployment;
@@ -41,6 +42,7 @@ export const DeploymentMenu = ({
...props
}: DeploymentMenuProps) => {
const client = useGQLClient();
const { toast, dismiss } = useToast();
const [changeToProduction, setChangeToProduction] = useState(false);
const [redeployToProduction, setRedeployToProduction] = useState(false);
@@ -51,9 +53,19 @@ export const DeploymentMenu = ({
const isUpdated = await client.updateDeploymentToProd(deployment.id);
if (isUpdated) {
await onUpdate();
toast.success('Deployment changed to production');
toast({
id: 'deployment_changed_to_production',
title: 'Deployment changed to production',
variant: 'success',
onDismiss: dismiss,
});
} else {
toast.error('Unable to change deployment to production');
toast({
id: 'deployment_not_changed_to_production',
title: 'Error changing deployment to production',
variant: 'error',
onDismiss: dismiss,
});
}
};
@@ -61,9 +73,19 @@ export const DeploymentMenu = ({
const isRedeployed = await client.redeployToProd(deployment.id);
if (isRedeployed) {
await onUpdate();
toast.success('Redeployed to production');
toast({
id: 'redeployed_to_production',
title: 'Redeployed to production',
variant: 'success',
onDismiss: dismiss,
});
} else {
toast.error('Unable to redeploy to production');
toast({
id: 'redeployed_to_production_failed',
title: 'Error redeploying to production',
variant: 'error',
onDismiss: dismiss,
});
}
};
@@ -74,9 +96,19 @@ export const DeploymentMenu = ({
);
if (isRollbacked) {
await onUpdate();
toast.success('Deployment rolled back');
toast({
id: 'deployment_rolled_back',
title: 'Deployment rolled back',
variant: 'success',
onDismiss: dismiss,
});
} else {
toast.error('Unable to rollback deployment');
toast({
id: 'deployment_rollback_failed',
title: 'Error rolling back deployment',
variant: 'error',
onDismiss: dismiss,
});
}
};
@@ -84,9 +116,19 @@ export const DeploymentMenu = ({
const isDeleted = await client.deleteDeployment(deployment.id);
if (isDeleted) {
await onUpdate();
toast.success('Deleted deployment');
toast({
id: 'deployment_deleted',
title: 'Deployment deleted',
variant: 'success',
onDismiss: dismiss,
});
} else {
toast.error('Unable to delete deployment');
toast({
id: 'deployment_not_deleted',
title: 'Error deleting deployment',
variant: 'error',
onDismiss: dismiss,
});
}
};
@@ -1,13 +1,13 @@
import { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { AddProjectMemberInput, Permission } from 'gql-client';
import { Typography } from '@snowballtools/material-tailwind-react-fork';
import { Button } from 'components/shared/Button';
import { Modal } from 'components/shared/Modal';
import { Input } from 'components/shared/Input';
import { Checkbox } from 'components/shared/Checkbox';
import { Select, SelectOption } from 'components/shared/Select';
import { AddProjectMemberInput, Permission } from 'gql-client';
interface AddMemberDialogProp {
open: boolean;
@@ -17,18 +17,30 @@ interface AddMemberDialogProp {
interface formData {
emailAddress: string;
permissions: {
view: boolean;
edit: boolean;
};
canEdit: boolean;
}
const permissionViewOptions: SelectOption = {
value: Permission.View,
label: Permission.View,
};
const permissionEditOptions: SelectOption = {
value: Permission.Edit,
label: Permission.Edit,
};
const permissionsDropdownOptions: SelectOption[] = [
permissionViewOptions,
permissionEditOptions,
];
const AddMemberDialog = ({
open,
handleOpen,
handleAddMember,
}: AddMemberDialogProp) => {
const {
watch,
setValue,
handleSubmit,
register,
reset,
@@ -36,10 +48,7 @@ const AddMemberDialog = ({
} = useForm({
defaultValues: {
emailAddress: '',
permissions: {
view: true,
edit: false,
},
canEdit: false,
},
});
@@ -47,11 +56,7 @@ const AddMemberDialog = ({
reset();
handleOpen();
const permissions = Object.entries(data.permissions)
.filter(([, value]) => value)
.map(
([key]) => key.charAt(0).toUpperCase() + key.slice(1),
) as Permission[];
const permissions = [data.canEdit ? Permission.Edit : Permission.View];
await handleAddMember({ email: data.emailAddress, permissions });
}, []);
@@ -72,19 +77,19 @@ const AddMemberDialog = ({
required: 'email field cannot be empty',
})}
/>
<Typography variant="small">Permissions</Typography>
<Typography variant="small">
You can change this later if required.
</Typography>
<Checkbox
label={Permission.View}
{...register(`permissions.view`)}
color="blue"
/>
<Checkbox
label={Permission.Edit}
{...register(`permissions.edit`)}
color="blue"
<Select
label="Permissions"
description="You can change this later if required."
options={permissionsDropdownOptions}
value={
watch('canEdit') ? permissionEditOptions : permissionViewOptions
}
onChange={(value) =>
setValue(
'canEdit',
(value as SelectOption)!.value === Permission.Edit,
)
}
/>
</Modal.Body>
<Modal.Footer>
@@ -4,7 +4,6 @@ import {
useMemo,
ComponentPropsWithoutRef,
} from 'react';
import { FieldValues, UseFormRegister } from 'react-hook-form';
import { WarningIcon } from 'components/shared/CustomIcon';
import { cloneIcon } from 'utils/cloneIcon';
@@ -12,7 +11,7 @@ import { cn } from 'utils/classnames';
import { InputTheme, inputTheme } from './Input.theme';
export interface InputProps<T extends FieldValues = FieldValues>
export interface InputProps
extends InputTheme,
Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
label?: string;
@@ -20,9 +19,6 @@ export interface InputProps<T extends FieldValues = FieldValues>
leftIcon?: ReactNode;
rightIcon?: ReactNode;
helperText?: string;
// react-hook-form optional register
register?: ReturnType<UseFormRegister<T>>;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
@@ -34,7 +30,6 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
leftIcon,
rightIcon,
helperText,
register,
size,
state,
appearance,
@@ -107,12 +102,11 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
<div className={containerCls({ class: className })}>
{leftIcon && renderLeftIcon}
<input
{...(register ? register : {})}
className={cn(inputCls(), {
'pl-10': leftIcon,
})}
{...props}
ref={ref}
{...props}
/>
{rightIcon && renderRightIcon}
</div>
@@ -20,6 +20,7 @@ import { cn } from 'utils/classnames';
import { useMediaQuery } from 'usehooks-ts';
import { SIDEBAR_MENU } from './constants';
import { UserSelect } from 'components/shared/UserSelect';
import { baseUrl } from 'utils/constants';
interface SidebarProps {
mobileOpen?: boolean;
@@ -83,7 +84,7 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
}, [orgSlug]);
const handleLogOut = useCallback(async () => {
await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/logout`, {
await fetch(`${baseUrl}/auth/logout`, {
method: 'POST',
credentials: 'include',
});
@@ -103,10 +103,12 @@ export const UserSelect = ({ options, value }: UserSelectProps) => {
<div className={theme.container()}>
{/* Input */}
<div
{...getToggleButtonProps({
ref: inputWrapperRef,
suppressRefError: true,
})}
{...getToggleButtonProps(
{
ref: inputWrapperRef,
},
{ suppressRefError: true },
)}
onClick={() => !dropdownOpen && openMenu()}
className="cursor-pointer relative py-2 pl-2 pr-4 flex min-w-[200px] w-full items-center justify-between rounded-xl bg-surface-card shadow-sm"
>
@@ -1,9 +1,11 @@
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { VITE_WALLET_CONNECT_ID } from 'utils/constants';
const queryClient = new QueryClient();
if (!import.meta.env.VITE_WALLET_CONNECT_ID) {
if (!VITE_WALLET_CONNECT_ID) {
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
}
+6 -2
View File
@@ -14,13 +14,17 @@ import { GQLClientProvider } from './context/GQLClientContext';
import { SERVER_GQL_PATH } from './constants';
import { Toaster } from 'components/shared/Toast';
import { LogErrorBoundary } from 'utils/log-error';
import { baseUrl } from 'utils/constants';
// @ts-ignore
console.log(`v-${__VERSION__}`);
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
assert(import.meta.env.VITE_SERVER_URL, 'VITE_SERVER_URL is not set in env');
const gqlEndpoint = `${import.meta.env.VITE_SERVER_URL}/${SERVER_GQL_PATH}`;
assert(baseUrl, 'VITE_SERVER_URL is not set in env');
const gqlEndpoint = `${baseUrl}/${SERVER_GQL_PATH}`;
const gqlClient = new GQLClient({ gqlEndpoint });
@@ -0,0 +1,94 @@
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 { Input } from 'components/shared/Input';
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);
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.length === 6;
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">
<Input
value={accessCode}
onChange={(e) => setAccessCode(e.target.value)}
disabled={!!loading}
/>
</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}
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
};
@@ -0,0 +1,20 @@
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,7 +47,9 @@ export const Login = ({ onDone }: Props) => {
console.log('Handling google redirect back');
try {
await google.handleOAuthRedirectBack();
// @ts-ignore
wallet = await google.getEthersWallet();
// @ts-ignore
const result = await signInWithEthereum(1, 'login', wallet);
if (result.error) {
setError(result.error);
@@ -21,6 +21,7 @@ import {
turnkeySignin,
turnkeySignup,
} from 'utils/turnkey-frontend';
import { verifyAccessCode } from 'utils/accessCode';
type Provider = 'google' | 'github' | 'apple' | 'email';
@@ -45,7 +46,9 @@ export const SignUp = ({ onDone }: Props) => {
setProvider('google');
try {
await google.handleOAuthRedirectBack();
// @ts-ignore
wallet = await google.getEthersWallet();
// @ts-ignore
const result = await signInWithEthereum(1, 'signup', wallet);
if (result.error) {
setError({ type: 'provider', message: result.error });
@@ -109,6 +112,29 @@ export const SignUp = ({ onDone }: Props) => {
const loading = provider;
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 (
<div>
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
@@ -1,14 +1,15 @@
import React, { useEffect, useState } from 'react';
import { snowball } from 'utils/use-snowball';
import { Login } from './Login';
import { SignUp } from './SignUp';
import { useEffect, useState } from 'react';
import { Done } from './Done';
import { AccessSignUp } from './AccessSignUp';
type Screen = 'login' | 'signup' | 'success';
const DASHBOARD_URL = '/';
export const SnowballAuth = () => {
export const SnowballAuth: React.FC = () => {
const path = window.location.pathname;
const [screen, setScreen] = useState<Screen>(
path === '/login' ? 'login' : 'signup',
@@ -30,7 +31,7 @@ export const SnowballAuth = () => {
if (screen === 'signup') {
return (
<SignUp
<AccessSignUp
onDone={() => {
setScreen('success');
}}
@@ -16,7 +16,7 @@ const FIRST_MEMBER_CARD = 0;
const CollaboratorsTabPanel = () => {
const client = useGQLClient();
const { toast } = useToast();
const { toast, dismiss } = useToast();
const { project } = useOutletContext<OutletContextType>();
const [addmemberDialogOpen, setAddMemberDialogOpen] = useState(false);
@@ -39,14 +39,14 @@ const CollaboratorsTabPanel = () => {
id: 'member_added',
title: 'Member added to project',
variant: 'success',
onDismiss() {},
onDismiss: dismiss,
});
} else {
toast({
id: 'member_not_added',
title: 'Invitation not sent',
variant: 'error',
onDismiss() {},
onDismiss: dismiss,
});
}
},
@@ -63,14 +63,14 @@ const CollaboratorsTabPanel = () => {
id: 'member_removed',
title: 'Member removed from project',
variant: 'success',
onDismiss() {},
onDismiss: dismiss,
});
} else {
toast({
id: 'member_not_removed',
title: 'Not able to remove member',
variant: 'error',
onDismiss() {},
onDismiss: dismiss,
});
}
};
@@ -86,14 +86,14 @@ const CollaboratorsTabPanel = () => {
id: 'member_permission_updated',
title: 'Project member permission updated',
variant: 'success',
onDismiss() {},
onDismiss: dismiss,
});
} else {
toast({
id: 'member_permission_not_updated',
title: 'Project member permission not updated',
variant: 'error',
onDismiss() {},
onDismiss: dismiss,
});
}
},
+28
View File
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,11 @@
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 BugsnagPluginReact from '@bugsnag/plugin-react';
import BugsnagPerformance from '@bugsnag/browser-performance';
import React from 'react';
const bugsnagApiKey = import.meta.env.VITE_BUGSNAG_API_KEY;
import { VITE_BUGSNAG_API_KEY } from './constants';
if (bugsnagApiKey) {
if (VITE_BUGSNAG_API_KEY) {
Bugsnag.start({
apiKey: bugsnagApiKey,
apiKey: VITE_BUGSNAG_API_KEY,
plugins: [new BugsnagPluginReact()],
});
BugsnagPerformance.start({ apiKey: bugsnagApiKey });
BugsnagPerformance.start({ apiKey: VITE_BUGSNAG_API_KEY });
}
export const errorLoggingEnabled = !!bugsnagApiKey;
export const errorLoggingEnabled = !!VITE_BUGSNAG_API_KEY;
export const LogErrorBoundary = bugsnagApiKey
export const LogErrorBoundary = VITE_BUGSNAG_API_KEY
? Bugsnag.getPlugin('react')!.createErrorBoundary(React)
: ({ children }: any) => children;
@@ -28,7 +28,7 @@ export function logError(error: Error) {
}
console.error(...errors);
if (import.meta.env.VITE_BUGSNAG_API_KEY) {
if (VITE_BUGSNAG_API_KEY) {
Bugsnag.notify(error);
}
}
+3 -1
View File
@@ -2,6 +2,8 @@ import { SiweMessage } from 'siwe';
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
import { v4 as uuid } from 'uuid';
import { baseUrl } from './constants';
const domain = window.location.host;
const origin = window.location.origin;
@@ -17,7 +19,7 @@ export async function signInWithEthereum(
);
const signature = await wallet.signMessage(message);
const res = await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/validate`, {
const res = await fetch(`${baseUrl}/auth/validate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -1,10 +1,7 @@
import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http';
import { WebauthnStamper } from '@turnkey/webauthn-stamper';
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!;
import { baseUrl, PASSKEY_WALLET_RPID, TURNKEY_BASE_URL } from './constants';
// All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
// We only support ES256, which is listed here
+4 -3
View File
@@ -5,16 +5,17 @@ import {
LitGoogleAuth,
LitPasskeyAuth,
} from '@snowballtools/auth-lit';
import { VITE_LIT_RELAY_API_KEY } from './constants';
export const snowball = Snowball.withAuth({
google: LitGoogleAuth.configure({
litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
}),
// apple: LitAppleAuth.configure({
// litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
// litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
// }),
passkey: LitPasskeyAuth.configure({
litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
}),
}).create({
initialChain: SnowballChain.sepolia,
+5
View File
@@ -1,5 +1,7 @@
import { defineConfig, PluginOption } from 'vite';
import react from '@vitejs/plugin-react';
import { exec } from 'child_process';
import { promisify } from 'util';
// https://vitejs.dev/config/
export default defineConfig({
@@ -16,5 +18,8 @@ export default defineConfig({
},
define: {
'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
+4 -1
View File
@@ -20,7 +20,10 @@ var __async = (__this, __arguments, generator) => {
};
// src/client.ts
import { ApolloClient, InMemoryCache } from "@apollo/client";
import {
ApolloClient,
InMemoryCache
} from "@apollo/client";
// src/queries.ts
import { gql } from "@apollo/client";
File diff suppressed because one or more lines are too long
+146 -89
View File
@@ -1,8 +1,13 @@
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
import {
ApolloClient,
DefaultOptions,
InMemoryCache,
NormalizedCacheObject,
} from "@apollo/client";
import * as queries from './queries';
import * as types from './types';
import * as mutations from './mutations';
import * as queries from "./queries";
import * as types from "./types";
import * as mutations from "./mutations";
export interface GraphQLConfig {
gqlEndpoint: string;
@@ -11,320 +16,372 @@ export interface GraphQLConfig {
// TODO: check options
const defaultOptions: DefaultOptions = {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'ignore'
fetchPolicy: "no-cache",
errorPolicy: "ignore",
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all'
}
fetchPolicy: "no-cache",
errorPolicy: "all",
},
};
export class GQLClient {
private client: ApolloClient<NormalizedCacheObject>;
constructor (config: GraphQLConfig) {
constructor(config: GraphQLConfig) {
this.client = new ApolloClient({
uri: config.gqlEndpoint,
cache: new InMemoryCache(),
defaultOptions,
credentials: 'include'
credentials: "include",
});
}
async getUser () : Promise<types.GetUserResponse> {
async getUser(): Promise<types.GetUserResponse> {
const { data } = await this.client.query({
query: queries.getUser
query: queries.getUser,
});
return data;
}
async getProject (projectId: string) : Promise<types.GetProjectResponse> {
async getProject(projectId: string): Promise<types.GetProjectResponse> {
const { data } = await this.client.query({
query: queries.getProject,
variables: {
projectId
}
projectId,
},
});
return data;
}
async getProjectsInOrganization (organizationSlug: string) : Promise<types.GetProjectsInOrganizationResponse> {
async getProjectsInOrganization(
organizationSlug: string
): Promise<types.GetProjectsInOrganizationResponse> {
const { data } = await this.client.query({
query: queries.getProjectsInOrganization,
variables: {
organizationSlug
}
organizationSlug,
},
});
return data;
}
async getOrganizations () : Promise<types.GetOrganizationsResponse> {
async getOrganizations(): Promise<types.GetOrganizationsResponse> {
const { data } = await this.client.query({
query: queries.getOrganizations
query: queries.getOrganizations,
});
return data;
}
async getDeployments (projectId: string) : Promise<types.GetDeploymentsResponse> {
async getDeployments(
projectId: string
): Promise<types.GetDeploymentsResponse> {
const { data } = await this.client.query({
query: queries.getDeployments,
variables: {
projectId
}
projectId,
},
});
return data;
}
async getEnvironmentVariables (projectId: string) : Promise<types.GetEnvironmentVariablesResponse> {
async getEnvironmentVariables(
projectId: string
): Promise<types.GetEnvironmentVariablesResponse> {
const { data } = await this.client.query({
query: queries.getEnvironmentVariables,
variables: {
projectId
}
projectId,
},
});
return data;
}
async getProjectMembers (projectId: string) : Promise<types.GetProjectMembersResponse> {
async getProjectMembers(
projectId: string
): Promise<types.GetProjectMembersResponse> {
const result = await this.client.query({
query: queries.getProjectMembers,
variables: {
projectId
}
projectId,
},
});
return result.data;
}
async addProjectMember (projectId: string, data: types.AddProjectMemberInput) : Promise<types.AddProjectMemberResponse> {
async addProjectMember(
projectId: string,
data: types.AddProjectMemberInput
): Promise<types.AddProjectMemberResponse> {
const result = await this.client.mutate({
mutation: mutations.addProjectMember,
variables: {
projectId,
data
}
data,
},
});
return result.data;
}
async updateProjectMember (projectMemberId: string, data: types.UpdateProjectMemberInput): Promise<types.UpdateProjectMemberResponse> {
async updateProjectMember(
projectMemberId: string,
data: types.UpdateProjectMemberInput
): Promise<types.UpdateProjectMemberResponse> {
const result = await this.client.mutate({
mutation: mutations.updateProjectMember,
variables: {
projectMemberId,
data
}
data,
},
});
return result.data;
}
async removeProjectMember (projectMemberId: string): Promise<types.RemoveProjectMemberResponse> {
async removeProjectMember(
projectMemberId: string
): Promise<types.RemoveProjectMemberResponse> {
const result = await this.client.mutate({
mutation: mutations.removeProjectMember,
variables: {
projectMemberId
}
projectMemberId,
},
});
return result.data;
}
async searchProjects (searchText: string) : Promise<types.SearchProjectsResponse> {
async searchProjects(
searchText: string
): Promise<types.SearchProjectsResponse> {
const { data } = await this.client.query({
query: queries.searchProjects,
variables: {
searchText
}
searchText,
},
});
return data;
}
async addEnvironmentVariables (projectId: string, data: types.AddEnvironmentVariableInput[]): Promise<types.AddEnvironmentVariablesResponse> {
async addEnvironmentVariables(
projectId: string,
data: types.AddEnvironmentVariableInput[]
): Promise<types.AddEnvironmentVariablesResponse> {
const result = await this.client.mutate({
mutation: mutations.addEnvironmentVariables,
variables: {
projectId,
data
}
data,
},
});
return result.data;
}
async updateEnvironmentVariable (environmentVariableId: string, data: types.UpdateEnvironmentVariableInput): Promise<types.UpdateEnvironmentVariableResponse> {
async updateEnvironmentVariable(
environmentVariableId: string,
data: types.UpdateEnvironmentVariableInput
): Promise<types.UpdateEnvironmentVariableResponse> {
const result = await this.client.mutate({
mutation: mutations.updateEnvironmentVariable,
variables: {
environmentVariableId,
data
}
data,
},
});
return result.data;
}
async removeEnvironmentVariable (environmentVariableId: string): Promise<types.RemoveEnvironmentVariableResponse> {
async removeEnvironmentVariable(
environmentVariableId: string
): Promise<types.RemoveEnvironmentVariableResponse> {
const { data } = await this.client.mutate({
mutation: mutations.removeEnvironmentVariable,
variables: {
environmentVariableId
}
environmentVariableId,
},
});
return data;
}
async updateDeploymentToProd (deploymentId: string): Promise<types.UpdateDeploymentToProdResponse> {
async updateDeploymentToProd(
deploymentId: string
): Promise<types.UpdateDeploymentToProdResponse> {
const { data } = await this.client.mutate({
mutation: mutations.updateDeploymentToProd,
variables: {
deploymentId
}
deploymentId,
},
});
return data;
}
async addProject (organizationSlug: string, data: types.AddProjectInput): Promise<types.AddProjectResponse> {
async addProject(
organizationSlug: string,
data: types.AddProjectInput
): Promise<types.AddProjectResponse> {
const result = await this.client.mutate({
mutation: mutations.addProject,
variables: {
organizationSlug,
data
}
data,
},
});
return result.data;
}
async updateProject (projectId: string, data: types.UpdateProjectInput): Promise<types.UpdateProjectResponse> {
async updateProject(
projectId: string,
data: types.UpdateProjectInput
): Promise<types.UpdateProjectResponse> {
const result = await this.client.mutate({
mutation: mutations.updateProjectMutation,
variables: {
projectId,
data
}
data,
},
});
return result.data;
}
async updateDomain (domainId: string, data: types.UpdateDomainInput): Promise<types.UpdateDomainResponse> {
async updateDomain(
domainId: string,
data: types.UpdateDomainInput
): Promise<types.UpdateDomainResponse> {
const result = await this.client.mutate({
mutation: mutations.updateDomainMutation,
variables: {
domainId,
data
}
data,
},
});
return result.data;
}
async redeployToProd (deploymentId: string): Promise<types.RedeployToProdResponse> {
async redeployToProd(
deploymentId: string
): Promise<types.RedeployToProdResponse> {
const { data } = await this.client.mutate({
mutation: mutations.redeployToProd,
variables: {
deploymentId
}
deploymentId,
},
});
return data;
}
async deleteProject (projectId: string): Promise<types.DeleteProjectResponse> {
async deleteProject(projectId: string): Promise<types.DeleteProjectResponse> {
const { data } = await this.client.mutate({
mutation: mutations.deleteProject,
variables: {
projectId
}
projectId,
},
});
return data;
}
async deleteDomain (domainId: string): Promise<types.DeleteDomainResponse> {
async deleteDomain(domainId: string): Promise<types.DeleteDomainResponse> {
const { data } = await this.client.mutate({
mutation: mutations.deleteDomain,
variables: {
domainId
}
domainId,
},
});
return data;
}
async rollbackDeployment (projectId: string, deploymentId: string): Promise<types.RollbackDeploymentResponse> {
async rollbackDeployment(
projectId: string,
deploymentId: string
): Promise<types.RollbackDeploymentResponse> {
const { data } = await this.client.mutate({
mutation: mutations.rollbackDeployment,
variables: {
projectId,
deploymentId
}
deploymentId,
},
});
return data;
}
async deleteDeployment (deploymentId: string): Promise<types.DeleteDeploymentResponse> {
async deleteDeployment(
deploymentId: string
): Promise<types.DeleteDeploymentResponse> {
const { data } = await this.client.mutate({
mutation: mutations.deleteDeployment,
variables: {
deploymentId
}
deploymentId,
},
});
return data;
}
async addDomain (projectId: string, data: types.AddDomainInput): Promise<types.AddDomainResponse> {
async addDomain(
projectId: string,
data: types.AddDomainInput
): Promise<types.AddDomainResponse> {
const result = await this.client.mutate({
mutation: mutations.addDomain,
variables: {
projectId,
data
}
data,
},
});
return result.data;
}
async getDomains (projectId: string, filter?: types.FilterDomainInput): Promise<types.GetDomainsResponse> {
async getDomains(
projectId: string,
filter?: types.FilterDomainInput
): Promise<types.GetDomainsResponse> {
const { data } = await this.client.query({
query: queries.getDomains,
variables: {
projectId,
filter
}
filter,
},
});
return data;
}
async authenticateGitHub (code: string): Promise<types.AuthenticateGitHubResponse> {
async authenticateGitHub(
code: string
): Promise<types.AuthenticateGitHubResponse> {
const { data } = await this.client.mutate({
mutation: mutations.authenticateGitHub,
variables: {
code
}
code,
},
});
return data;
}
async unauthenticateGithub (): Promise<types.UnauthenticateGitHubResponse> {
async unauthenticateGithub(): Promise<types.UnauthenticateGitHubResponse> {
const { data } = await this.client.mutate({
mutation: mutations.unauthenticateGitHub
mutation: mutations.unauthenticateGitHub,
});
return data;
+5
View File
@@ -10575,6 +10575,11 @@ exponential-backoff@^3.1.1:
resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6"
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:
version "1.18.0"
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.0.tgz#a6ae39d9091f2efba5f20fc5c65a3ce7c9ce16a3"