mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-22 21:50:43 +03:00
Merge pull request #24 from twentyhq/charles-bochet-simplify-infra
Simplify infrastructure to one container
This commit is contained in:
commit
eac43d1e64
@ -6,13 +6,8 @@ orbs:
|
||||
slack: circleci/slack@4.12.0
|
||||
node: circleci/node@5.0.3
|
||||
|
||||
parameters:
|
||||
build-api:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
tests-server:
|
||||
tests-front:
|
||||
executor: node/default
|
||||
steps:
|
||||
- checkout
|
||||
@ -22,31 +17,19 @@ jobs:
|
||||
- run:
|
||||
command: cd front && npm run test
|
||||
name: tests
|
||||
deploy-api-canary:
|
||||
docker:
|
||||
- image: hasura/graphql-engine:latest.cli-migrations-v3
|
||||
steps:
|
||||
- checkout
|
||||
- run: cd api && /bin/hasura-cli deploy --admin-secret $HASURA_ADMIN_SECRET --endpoint $HASURA_CANARY_ENDPOINT
|
||||
deploy-api:
|
||||
docker:
|
||||
- image: hasura/graphql-engine:latest.cli-migrations-v3
|
||||
steps:
|
||||
- checkout
|
||||
- run: cd api && /bin/hasura-cli metadata apply --admin-secret $HASURA_ADMIN_SECRET --endpoint $HASURA_ENDPOINT
|
||||
|
||||
workflows:
|
||||
build-and-deploy-server:
|
||||
build-and-deploy:
|
||||
jobs:
|
||||
- tests-server
|
||||
- tests-front
|
||||
- aws-ecr/build-and-push-image:
|
||||
name: build-image-server
|
||||
name: build-image
|
||||
filters:
|
||||
branches:
|
||||
only: main
|
||||
requires:
|
||||
- tests-server
|
||||
dockerfile: ./infra/prod/twenty-server/Dockerfile
|
||||
- tests-front
|
||||
dockerfile: ./infra/prod/twenty/Dockerfile
|
||||
registry-id: AWS_ACCOUNT_ID
|
||||
aws-access-key-id: AWS_ACCESS_KEY_ID
|
||||
aws-secret-access-key: AWS_SECRET_ACCESS_KEY
|
||||
@ -54,35 +37,31 @@ workflows:
|
||||
repo: $AWS_ECR_REPO
|
||||
tag: $CIRCLE_SHA1
|
||||
extra-build-args: >
|
||||
--build-arg REACT_APP_AUTH0_DOMAIN=$REACT_APP_AUTH0_DOMAIN
|
||||
--build-arg REACT_APP_AUTH0_CLIENT_ID=$REACT_APP_AUTH0_CLIENT_ID
|
||||
--build-arg REACT_APP_AUTH0_CALLBACK_URL=$REACT_APP_AUTH0_CALLBACK_URL
|
||||
--build-arg REACT_APP_AUTH0_AUDIENCE=$REACT_APP_AUTH0_AUDIENCE
|
||||
--build-arg REACT_APP_API_URL=$REACT_APP_API_URL
|
||||
|
||||
- aws-ecs/deploy-service-update:
|
||||
name: deploy-server-canary
|
||||
name: deploy-canary
|
||||
requires:
|
||||
- build-image-server
|
||||
family: $AWS_ECS_CONTAINER_NAME_SERVER_CANARY
|
||||
- build-image
|
||||
family: $AWS_ECS_CONTAINER_NAME_CANARY
|
||||
cluster: $AWS_ECS_CLUSTER
|
||||
container-image-name-updates: "container=$AWS_ECS_CONTAINER_NAME_SERVER_CANARY,tag=${CIRCLE_SHA1}"
|
||||
container-image-name-updates: "container=$AWS_ECS_CONTAINER_NAME_CANARY,tag=${CIRCLE_SHA1}"
|
||||
- slack/on-hold:
|
||||
name: slack-notification
|
||||
context: slack-secrets
|
||||
requires:
|
||||
- deploy-server-canary
|
||||
- deploy-canary
|
||||
- hold:
|
||||
type: approval
|
||||
requires:
|
||||
- slack-notification
|
||||
- aws-ecs/deploy-service-update:
|
||||
name: deploy-server-default
|
||||
name: deploy-default
|
||||
requires:
|
||||
- hold
|
||||
family: $AWS_ECS_CONTAINER_NAME_SERVER_DEFAULT
|
||||
family: $AWS_ECS_CONTAINER_NAME_DEFAULT
|
||||
cluster: $AWS_ECS_CLUSTER
|
||||
container-image-name-updates: "container=$AWS_ECS_CONTAINER_NAME_SERVER_DEFAULT,tag=${CIRCLE_SHA1}"
|
||||
container-image-name-updates: "container=$AWS_ECS_CONTAINER_NAME_DEFAULT,tag=${CIRCLE_SHA1}"
|
||||
post-steps:
|
||||
- slack/notify:
|
||||
event: pass
|
||||
@ -93,8 +72,8 @@ workflows:
|
||||
- aws-ecr/build-and-push-image:
|
||||
name: build-image-latest
|
||||
requires:
|
||||
- deploy-server-default
|
||||
dockerfile: ./infra/prod/twenty-server/Dockerfile
|
||||
- deploy-default
|
||||
dockerfile: ./infra/prod/twenty/Dockerfile
|
||||
registry-id: AWS_ACCOUNT_ID
|
||||
aws-access-key-id: AWS_ACCESS_KEY_ID
|
||||
aws-secret-access-key: AWS_SECRET_ACCESS_KEY
|
||||
@ -102,68 +81,4 @@ workflows:
|
||||
repo: $AWS_ECR_REPO
|
||||
tag: latest
|
||||
extra-build-args: >
|
||||
--build-arg REACT_APP_AUTH0_DOMAIN=$REACT_APP_AUTH0_DOMAIN
|
||||
--build-arg REACT_APP_AUTH0_CLIENT_ID=$REACT_APP_AUTH0_CLIENT_ID
|
||||
--build-arg REACT_APP_AUTH0_CALLBACK_URL=$REACT_APP_AUTH0_CALLBACK_URL
|
||||
--build-arg REACT_APP_AUTH0_AUDIENCE=$REACT_APP_AUTH0_AUDIENCE
|
||||
--build-arg REACT_APP_API_URL=$REACT_APP_API_URL
|
||||
|
||||
deploy-api:
|
||||
jobs:
|
||||
- deploy-api-canary:
|
||||
filters:
|
||||
branches:
|
||||
only: main
|
||||
- slack/on-hold:
|
||||
name: slack-notification
|
||||
context: slack-secrets
|
||||
requires:
|
||||
- deploy-api-canary
|
||||
- hold:
|
||||
type: approval
|
||||
requires:
|
||||
- slack-notification
|
||||
- deploy-api:
|
||||
requires:
|
||||
- hold
|
||||
build-api:
|
||||
when: << pipeline.parameters.build-api >>
|
||||
jobs:
|
||||
- aws-ecr/build-and-push-image:
|
||||
name: build-image-api
|
||||
dockerfile: ./infra/prod/twenty-api/Dockerfile
|
||||
registry-id: AWS_ACCOUNT_ID
|
||||
aws-access-key-id: AWS_ACCESS_KEY_ID
|
||||
aws-secret-access-key: AWS_SECRET_ACCESS_KEY
|
||||
region: $AWS_REGION
|
||||
repo: $AWS_ECR_REPO_API
|
||||
tag: $CIRCLE_SHA1
|
||||
- aws-ecs/deploy-service-update:
|
||||
name: deploy-api-canary
|
||||
requires:
|
||||
- build-image-api
|
||||
family: $AWS_ECS_CONTAINER_NAME_API_CANARY
|
||||
cluster: $AWS_ECS_CLUSTER
|
||||
container-image-name-updates: "container=$AWS_ECS_CONTAINER_NAME_API_CANARY,tag=${CIRCLE_SHA1}"
|
||||
- hold:
|
||||
type: approval
|
||||
requires:
|
||||
- deploy-api-canary
|
||||
- aws-ecs/deploy-service-update:
|
||||
name: deploy-api-default
|
||||
requires:
|
||||
- hold
|
||||
family: $AWS_ECS_CONTAINER_NAME_API_DEFAULT
|
||||
cluster: $AWS_ECS_CLUSTER
|
||||
container-image-name-updates: "container=$AWS_ECS_CONTAINER_NAME_API_DEFAULT,tag=${CIRCLE_SHA1}"
|
||||
- aws-ecr/build-and-push-image:
|
||||
name: build-image-api-latest
|
||||
requires:
|
||||
- deploy-api-default
|
||||
dockerfile: ./infra/prod/twenty-api/Dockerfile
|
||||
registry-id: AWS_ACCOUNT_ID
|
||||
aws-access-key-id: AWS_ACCESS_KEY_ID
|
||||
aws-secret-access-key: AWS_SECRET_ACCESS_KEY
|
||||
region: $AWS_REGION
|
||||
repo: $AWS_ECR_REPO_API
|
||||
tag: latest
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.vscode/*
|
||||
**/**/.env
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
node_modules/**/**
|
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 twentyhq
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
83
README.md
83
README.md
@ -4,16 +4,29 @@ Welcome to Twenty documentation!
|
||||
|
||||
## High Level Overview
|
||||
|
||||
Twenty development stack is composed of 5 different layers:
|
||||
- twenty-front: our frontend React app
|
||||
- twenty-api (Hasura): our backend presentation layer that can do straight forward CRUDs, permissionning, authentication.
|
||||
- twenty-server: our backend that contain complex logics, scripts, jobs...
|
||||
- [tbd] twenty-events (Jitsu): our event ingestor which is separated from api and server to ensure high availability
|
||||
- storages: postgres, [tbd] elasticsearch, [tbd] redis.
|
||||
Twenty development stack is composed of 3 different layers
|
||||
- front: our frontend React app
|
||||
- server: our backend that contain endpoint, crm logic, scripts, jobs...
|
||||
- storages: postgres
|
||||
|
||||
## Development environment setup
|
||||
## Development environment setup with npm (Alternative 1)
|
||||
|
||||
This section only discusses the development setup. The whole developemnt environment is containerized with Docker and orchestrated with docker-compose.
|
||||
This is the easiest way to get started contributing to twenty
|
||||
Make sure you have `node@18` installed on your machine. You can use `nvm` to manage your nvm versions in case you have projects that require different node versions.
|
||||
|
||||
`npm install`
|
||||
`npm start`
|
||||
|
||||
You'll need to provide your own postgres storage.
|
||||
|
||||
Once this is completed you should have:
|
||||
- front available on: http://localhost:3001
|
||||
- server available on: http://localhost:3000/health
|
||||
|
||||
|
||||
## Development environment setup with docker-compose (Alternative 2)
|
||||
|
||||
We also provide a containerized environment with Docker and orchestrated with docker-compose in case it is easier for you. This install will also provision a postgres container out of the box.
|
||||
|
||||
### Step 1: pre-requesites
|
||||
Make sure to have the latest Docker and Docker-compose versions installed on your computer.
|
||||
@ -31,10 +44,9 @@ docker-compose up --build --force-recreate
|
||||
```
|
||||
|
||||
Once this is completed you should have:
|
||||
- twenty-front available on: http://localhost:3001
|
||||
- twenty-api available on: http://localhost:8080
|
||||
- twenty-server available on: http://localhost:3000/health
|
||||
- postgres: available on http://localhost:5432 that should contain two database: twenty (data) and hasura (metadata)
|
||||
- front available on: http://localhost:3001
|
||||
- server available on: http://localhost:3000/health
|
||||
- postgres: available on http://localhost:5432 that should contain `twenty` database
|
||||
|
||||
### Step 3: environment file
|
||||
Configure your environment by copying the `.env.example` file located in `infra/dev` folder into `.env`.
|
||||
@ -44,34 +56,16 @@ cp infra/dev/.env.example infra/dev/.env
|
||||
|
||||
Then, you'll need to replace all REPLACE_ME variable by their development value. Please reach out to another engineer to get these values (as most of them are third party credentials, sensitive data)
|
||||
|
||||
### Step 4: API (Hasura) metadata
|
||||
Browse Hasura console on http://localhost:8080, go to settings and import metadata file located in `infra/dev/twenty-api` folder
|
||||
### Note
|
||||
|
||||
## Developping on Frontend
|
||||
If you are using Docker install, make sure to ssh in the docker container during development to execute commands. You can also use `Makefile` to help you
|
||||
|
||||
The whole development experience is happening in `infra/dev` folder.
|
||||
```
|
||||
cd infra/dev
|
||||
```
|
||||
|
||||
The development FE server is running on docker up and is exposing the `twenty-front` on port http://localhost:3001. As you modify the `/front` folder on your computer, this folder is synced with your `twenty-front` container and the frontend application is automatically refreshed.
|
||||
|
||||
### Develop
|
||||
|
||||
Recommended: as you modify frontend code, here is how to access `twenty-front` server logs in order to debug / watch typescript issues:
|
||||
```
|
||||
make up
|
||||
make logs container=twenty-front
|
||||
```
|
||||
|
||||
### Open a shell into the container
|
||||
```
|
||||
make front-sh
|
||||
```
|
||||
## Development
|
||||
|
||||
### Tests
|
||||
|
||||
#### Unit tests:
|
||||
|
||||
```
|
||||
make front-test
|
||||
# coverage
|
||||
@ -83,27 +77,6 @@ make front-coverage
|
||||
make front-storybook
|
||||
```
|
||||
|
||||
## Developping on API
|
||||
|
||||
The whole development experience is happening in `infra/dev` folder.
|
||||
```
|
||||
cd infra/dev
|
||||
```
|
||||
|
||||
The API is a Hasura instance which is a no-code container.
|
||||
To modify API behavior, you'll need to connect to run Hasura console through the CLI
|
||||
```
|
||||
make api-console
|
||||
```
|
||||
|
||||
Once your local changes in the console are finished you can export them into the filesystem (in order to version them, and deploy them to other environments), depending on your needs:
|
||||
|
||||
```
|
||||
make api-make-migration name=my_migration_name
|
||||
make api-make-metadata
|
||||
make api-make-seeds
|
||||
```
|
||||
|
||||
## Developping on server
|
||||
|
||||
Section TBD
|
@ -1,6 +0,0 @@
|
||||
version: 3
|
||||
endpoint: http://localhost:8080
|
||||
metadata_directory: metadata
|
||||
actions:
|
||||
kind: synchronous
|
||||
handler_webhook_baseurl: http://localhost:3000
|
@ -1,6 +0,0 @@
|
||||
actions: []
|
||||
custom_types:
|
||||
enums: []
|
||||
input_objects: []
|
||||
objects: []
|
||||
scalars: []
|
@ -1 +0,0 @@
|
||||
[]
|
@ -1 +0,0 @@
|
||||
{}
|
@ -1 +0,0 @@
|
||||
{}
|
@ -1 +0,0 @@
|
||||
[]
|
@ -1,9 +0,0 @@
|
||||
- name: twenty
|
||||
kind: postgres
|
||||
configuration:
|
||||
connection_info:
|
||||
database_url:
|
||||
from_env: HASURA_GRAPHQL_PG_DATABASE_URL
|
||||
isolation_level: read-committed
|
||||
use_prepared_statements: false
|
||||
tables: "!include twenty/tables/tables.yaml"
|
@ -1,31 +0,0 @@
|
||||
table:
|
||||
name: tenants
|
||||
schema: public
|
||||
array_relationships:
|
||||
- name: users
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: tenant_id
|
||||
table:
|
||||
name: users
|
||||
schema: public
|
||||
select_permissions:
|
||||
- role: public
|
||||
permission:
|
||||
columns:
|
||||
- auth0_client_id
|
||||
- domain
|
||||
filter: {}
|
||||
limit: 1
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- auth0_client_id
|
||||
- email_domain
|
||||
- id
|
||||
- name
|
||||
- uuid
|
||||
filter:
|
||||
users:
|
||||
email:
|
||||
_eq: X-Hasura-User-Email
|
@ -1,23 +0,0 @@
|
||||
table:
|
||||
name: users
|
||||
schema: public
|
||||
object_relationships:
|
||||
- name: tenant
|
||||
using:
|
||||
foreign_key_constraint_on: tenant_id
|
||||
select_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- id
|
||||
- tenant_id
|
||||
- email
|
||||
- first_name
|
||||
- last_name
|
||||
- created_at
|
||||
- updated_at
|
||||
filter:
|
||||
tenant:
|
||||
users:
|
||||
email:
|
||||
_eq: X-Hasura-User-Email
|
@ -1,2 +0,0 @@
|
||||
- "!include public_tenants.yaml"
|
||||
- "!include public_users.yaml"
|
@ -1 +0,0 @@
|
||||
disabled_for_roles: []
|
@ -1 +0,0 @@
|
||||
[]
|
@ -1 +0,0 @@
|
||||
{}
|
@ -1 +0,0 @@
|
||||
{}
|
@ -1 +0,0 @@
|
||||
{}
|
@ -1 +0,0 @@
|
||||
[]
|
@ -1 +0,0 @@
|
||||
[]
|
@ -1 +0,0 @@
|
||||
[]
|
@ -1 +0,0 @@
|
||||
version: 3
|
@ -1,57 +0,0 @@
|
||||
SET check_function_bodies = false;
|
||||
CREATE FUNCTION public.set_current_timestamp_updated_at() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
_new record;
|
||||
BEGIN
|
||||
_new := NEW;
|
||||
_new."updated_at" = NOW();
|
||||
RETURN _new;
|
||||
END;
|
||||
$$;
|
||||
CREATE TABLE public.tenants (
|
||||
id integer NOT NULL,
|
||||
name text NOT NULL,
|
||||
uuid uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
email_domain text NOT NULL
|
||||
);
|
||||
CREATE SEQUENCE public.tenants_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
ALTER SEQUENCE public.tenants_id_seq OWNED BY public.tenants.id;
|
||||
CREATE TABLE public.users (
|
||||
id integer NOT NULL,
|
||||
email text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
first_name text NOT NULL,
|
||||
last_name text NOT NULL,
|
||||
tenant_id integer NOT NULL
|
||||
);
|
||||
CREATE SEQUENCE public.users_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
|
||||
ALTER TABLE ONLY public.tenants ALTER COLUMN id SET DEFAULT nextval('public.tenants_id_seq'::regclass);
|
||||
ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
|
||||
ALTER TABLE ONLY public.tenants
|
||||
ADD CONSTRAINT tenants_email_domain_key UNIQUE (email_domain);
|
||||
ALTER TABLE ONLY public.tenants
|
||||
ADD CONSTRAINT tenants_pkey PRIMARY KEY (id);
|
||||
ALTER TABLE ONLY public.tenants
|
||||
ADD CONSTRAINT tenants_uuid_key UNIQUE (uuid);
|
||||
ALTER TABLE ONLY public.users
|
||||
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
||||
CREATE TRIGGER set_public_users_updated_at BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();
|
||||
COMMENT ON TRIGGER set_public_users_updated_at ON public.users IS 'trigger to set value of column "updated_at" to current timestamp on row update';
|
||||
ALTER TABLE ONLY public.users
|
||||
ADD CONSTRAINT users_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
@ -1 +0,0 @@
|
||||
alter table "public"."tenants" drop column "auth0_client_id";
|
@ -1 +0,0 @@
|
||||
alter table "public"."tenants" add column "auth0_client_id" text null;
|
@ -1 +0,0 @@
|
||||
alter table "public"."tenants" drop column "domain";
|
@ -1,2 +0,0 @@
|
||||
alter table "public"."tenants" add column "domain" text
|
||||
null default 'pilot.twenty.com';
|
@ -1,4 +0,0 @@
|
||||
SET check_function_bodies = false;
|
||||
INSERT INTO public.tenants (id, name, uuid, email_domain) VALUES (1, 'pilot', '8375f69d-47bd-4baa-a3c1-f8aaef8d8b2b', 'twenty.com');
|
||||
INSERT INTO public.tenants (id, name, uuid, email_domain) VALUES (2, 'ouihelp', 'c71becee-2cd6-4b31-827e-6cbef4e66879', 'ouihelp.twenty.com');
|
||||
SELECT pg_catalog.setval('public.tenants_id_seq', 2, true);
|
@ -1,6 +0,0 @@
|
||||
SET check_function_bodies = false;
|
||||
INSERT INTO public.users (id, email, created_at, updated_at, first_name, last_name, tenant_id) VALUES (1, 'charles@twenty.com', '2023-01-31 16:46:43.02666+00', '2023-01-31 16:46:43.02666+00', 'Charles', 'Bochet', 1);
|
||||
INSERT INTO public.users (id, email, created_at, updated_at, first_name, last_name, tenant_id) VALUES (2, 'charles@ouihelp.twenty.com', '2023-01-31 16:46:49.72368+00', '2023-01-31 16:46:49.72368+00', 'Charles', 'Bochet', 2);
|
||||
INSERT INTO public.users (id, email, created_at, updated_at, first_name, last_name, tenant_id) VALUES (3, 'felix@twenty.com', '2023-01-31 16:47:06.516066+00', '2023-01-31 16:47:06.516066+00', 'Félix', 'Malfait', 1);
|
||||
INSERT INTO public.users (id, email, created_at, updated_at, first_name, last_name, tenant_id) VALUES (4, 'felix@ouihelp.twenty.com', '2023-01-31 16:47:13.684386+00', '2023-01-31 16:47:13.684386+00', 'Félix', 'Malfait', 2);
|
||||
SELECT pg_catalog.setval('public.users_id_seq', 4, true);
|
2726
front/package-lock.json
generated
2726
front/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,6 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.5",
|
||||
"@auth0/auth0-react": "^2.0.0",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
|
@ -1,65 +1,29 @@
|
||||
import { Auth0Provider } from '@auth0/auth0-react';
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import Inbox from './pages/inbox/Inbox';
|
||||
import Contacts from './pages/Contacts';
|
||||
import Insights from './pages/Insights';
|
||||
import AuthCallback from './pages/AuthCallback';
|
||||
import AppLayout from './layout/AppLayout';
|
||||
import RequireAuth from './components/RequireAuth';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { useGetProfile } from './hooks/profile/useGetProfile';
|
||||
import { useGetTenantByDomain } from './hooks/tenant/useGetTenantByDomain';
|
||||
|
||||
function App() {
|
||||
const { tenant } = useGetTenantByDomain();
|
||||
const { user } = useGetProfile();
|
||||
const user = {
|
||||
id: 1,
|
||||
email: 'charles@twenty.com',
|
||||
first_name: 'Charles',
|
||||
last_name: 'Bochet',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tenant && (
|
||||
<Auth0Provider
|
||||
domain={process.env.REACT_APP_AUTH0_DOMAIN || ''}
|
||||
clientId={tenant?.auth0_client_id || ''}
|
||||
authorizationParams={{
|
||||
redirect_uri:
|
||||
window.location.protocol +
|
||||
'//' +
|
||||
window.location.host +
|
||||
process.env.REACT_APP_AUTH0_CALLBACK_URL || '',
|
||||
audience: process.env.REACT_APP_AUTH0_AUDIENCE || '',
|
||||
}}
|
||||
>
|
||||
<AppLayout user={user}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<Inbox />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/contacts"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<Contacts />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/insights"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<Insights />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
</Routes>
|
||||
</AppLayout>
|
||||
</Auth0Provider>
|
||||
)}
|
||||
{
|
||||
<AppLayout user={user}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Inbox />} />
|
||||
<Route path="/contacts" element={<Contacts />} />
|
||||
<Route path="/insights" element={<Insights />} />
|
||||
</Routes>
|
||||
</AppLayout>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { redirectIfNotLoggedIn } from '../hooks/AuthenticationHooks';
|
||||
|
||||
function RequireAuth({ children }: { children: JSX.Element }): JSX.Element {
|
||||
redirectIfNotLoggedIn();
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export default RequireAuth;
|
@ -1,14 +0,0 @@
|
||||
import RequireAuth from '../RequireAuth';
|
||||
|
||||
import Navbar from '../RequireAuth';
|
||||
|
||||
export default {
|
||||
title: 'RequireAuth',
|
||||
component: Navbar,
|
||||
};
|
||||
|
||||
export const RequireAuthWithHelloChild = () => (
|
||||
<RequireAuth>
|
||||
<div>Hello</div>
|
||||
</RequireAuth>
|
||||
);
|
@ -1,9 +0,0 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { RequireAuthWithHelloChild } from '../__stories__/RequireAuth.stories';
|
||||
|
||||
it('Checks the Require Auth renders', () => {
|
||||
const { getAllByText } = render(<RequireAuthWithHelloChild />);
|
||||
|
||||
expect(getAllByText('Hello')).toBeTruthy();
|
||||
});
|
@ -1,57 +0,0 @@
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import jwt from 'jwt-decode';
|
||||
import { TokenPayload } from '../interfaces/TokenPayload.interface';
|
||||
|
||||
const useIsNotLoggedIn = () => {
|
||||
const { isAuthenticated, isLoading } = useAuth0();
|
||||
const hasAccessToken = localStorage.getItem('accessToken');
|
||||
return (!isAuthenticated || !hasAccessToken) && !isLoading;
|
||||
};
|
||||
|
||||
const redirectIfNotLoggedIn = () => {
|
||||
const isNotLoggedIn = useIsNotLoggedIn();
|
||||
const { loginWithRedirect } = useAuth0();
|
||||
if (isNotLoggedIn) {
|
||||
loginWithRedirect();
|
||||
}
|
||||
};
|
||||
|
||||
const useGetUserEmailFromToken = (): string | undefined => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
|
||||
const payload: TokenPayload | undefined = token ? jwt(token) : undefined;
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
return payload['https://hasura.io/jwt/claims']['x-hasura-user-email'];
|
||||
};
|
||||
|
||||
const useGetAccessToken = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [token, setToken] = useState('');
|
||||
const { getAccessTokenSilently } = useAuth0();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchToken = async () => {
|
||||
setLoading(true);
|
||||
const accessToken = await getAccessTokenSilently();
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
|
||||
setLoading(false);
|
||||
setToken(accessToken);
|
||||
};
|
||||
|
||||
fetchToken();
|
||||
}, []);
|
||||
|
||||
return { loading, token };
|
||||
};
|
||||
|
||||
export {
|
||||
useIsNotLoggedIn,
|
||||
useGetAccessToken,
|
||||
redirectIfNotLoggedIn,
|
||||
useGetUserEmailFromToken,
|
||||
};
|
@ -1,129 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import {
|
||||
useIsNotLoggedIn,
|
||||
useGetUserEmailFromToken,
|
||||
} from '../AuthenticationHooks';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { mocked } from 'jest-mock';
|
||||
|
||||
jest.mock('@auth0/auth0-react');
|
||||
const mockedUseAuth0 = mocked(useAuth0, true);
|
||||
|
||||
const user = {
|
||||
email: 'johndoe@me.com',
|
||||
email_verified: true,
|
||||
sub: 'google-oauth2|12345678901234',
|
||||
};
|
||||
|
||||
describe('useIsNotLoggedIn', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('returns false if auth0 is loading', () => {
|
||||
mockedUseAuth0.mockReturnValue({
|
||||
isAuthenticated: false,
|
||||
user,
|
||||
logout: jest.fn(),
|
||||
loginWithRedirect: jest.fn(),
|
||||
getAccessTokenWithPopup: jest.fn(),
|
||||
getAccessTokenSilently: jest.fn(),
|
||||
getIdTokenClaims: jest.fn(),
|
||||
loginWithPopup: jest.fn(),
|
||||
handleRedirectCallback: jest.fn(),
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||
const isNotLoggedIn = result.current;
|
||||
|
||||
expect(isNotLoggedIn).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if token is not there', () => {
|
||||
mockedUseAuth0.mockReturnValue({
|
||||
isAuthenticated: false,
|
||||
user,
|
||||
logout: jest.fn(),
|
||||
loginWithRedirect: jest.fn(),
|
||||
getAccessTokenWithPopup: jest.fn(),
|
||||
getAccessTokenSilently: jest.fn(),
|
||||
getIdTokenClaims: jest.fn(),
|
||||
loginWithPopup: jest.fn(),
|
||||
handleRedirectCallback: jest.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||
const isNotLoggedIn = result.current;
|
||||
|
||||
expect(isNotLoggedIn).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if token is there but user is not connected on auth0', () => {
|
||||
mockedUseAuth0.mockReturnValue({
|
||||
isAuthenticated: false,
|
||||
user,
|
||||
logout: jest.fn(),
|
||||
loginWithRedirect: jest.fn(),
|
||||
getAccessTokenWithPopup: jest.fn(),
|
||||
getAccessTokenSilently: jest.fn(),
|
||||
getIdTokenClaims: jest.fn(),
|
||||
loginWithPopup: jest.fn(),
|
||||
handleRedirectCallback: jest.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
window.localStorage.setItem('accessToken', 'token');
|
||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||
const isNotLoggedIn = result.current;
|
||||
|
||||
expect(isNotLoggedIn).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if token is there and user is connected on auth0', () => {
|
||||
mockedUseAuth0.mockReturnValue({
|
||||
isAuthenticated: true,
|
||||
user,
|
||||
logout: jest.fn(),
|
||||
loginWithRedirect: jest.fn(),
|
||||
getAccessTokenWithPopup: jest.fn(),
|
||||
getAccessTokenSilently: jest.fn(),
|
||||
getIdTokenClaims: jest.fn(),
|
||||
loginWithPopup: jest.fn(),
|
||||
handleRedirectCallback: jest.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
window.localStorage.setItem('accessToken', 'token');
|
||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||
const isNotLoggedIn = result.current;
|
||||
|
||||
expect(isNotLoggedIn).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useGetUserEmailFromToken', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('returns undefined if token is not there', () => {
|
||||
const { result } = renderHook(() => useGetUserEmailFromToken());
|
||||
const email = result.current;
|
||||
|
||||
expect(email).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns email if token is there', () => {
|
||||
window.localStorage.setItem(
|
||||
'accessToken',
|
||||
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1FQXZiR0dFNjJ4S25mTFNxNHQ0dCJ9.eyJodHRwczovL2hhc3VyYS5pby9qd3QvY2xhaW1zIjp7IngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsidXNlciJdLCJ4LWhhc3VyYS1kZWZhdWx0LXJvbGUiOiJ1c2VyIiwieC1oYXN1cmEtdXNlci1lbWFpbCI6ImNoYXJsZXNAb3VpaGVscC50d2VudHkuY29tIiwieC1oYXN1cmEtdXNlci1pZCI6Imdvb2dsZS1vYXV0aDJ8MTE4MjM1ODk3NDQ2OTIwNTQ3NzMzIn0sImlzcyI6Imh0dHBzOi8vdHdlbnR5LWRldi5ldS5hdXRoMC5jb20vIiwic3ViIjoiZ29vZ2xlLW9hdXRoMnwxMTgyMzU4OTc0NDY5MjA1NDc3MzMiLCJhdWQiOlsiaGFzdXJhLWRldiIsImh0dHBzOi8vdHdlbnR5LWRldi5ldS5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNjc1MzYyNzY0LCJleHAiOjE2NzU0NDkxNjQsImF6cCI6IlM2ZXoyUFdUdUFsRncydjdxTFBWb2hmVXRseHc4QlBhIiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCJ9.DseeSqYzNlYVQfuicoK8fK1Z6b-TYNvCkRoXXYOhg1X3HDSejowUTudyrJGErkT65xMCfx8K5quof9eV8BZQixCPr670r5gAIHxHuGY_KNfHTOALe8E5VyQaoekRyDr99Qo3QxliOOlJxtmckA8FTeD6JanfVmcrqghUOIsSXXDOOzJV6eME7JErEjTQHpfxveSVbPlCmIqZ3fqDaFdKfAlUDZFhVQM8XbfubNmG4VcoMyB7H47yLdGkYvVfPO1lVg0efywQo4IfbtiqFv5CjOEqO6PG78Wfkd24bcilkf6ZuGXsA-w-0xlU089GhKF99lNI1PxvNWAaLFbqanxiEw',
|
||||
);
|
||||
const { result } = renderHook(() => useGetUserEmailFromToken());
|
||||
|
||||
expect(result.current).toBe('charles@ouihelp.twenty.com');
|
||||
});
|
||||
});
|
@ -1,25 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useQuery, QueryResult } from '@apollo/client';
|
||||
import { useGetProfile } from '../useGetProfile';
|
||||
|
||||
jest.mock('@apollo/client', () => ({
|
||||
useQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useGetUserEmailFromToken', () => {
|
||||
beforeEach(() => {
|
||||
const result: Partial<QueryResult<any>> = {
|
||||
data: { users: [{ email: 'test@twenty.com' }] },
|
||||
loading: false,
|
||||
error: undefined,
|
||||
};
|
||||
(useQuery as jest.Mock).mockImplementation(() => result as QueryResult);
|
||||
});
|
||||
|
||||
it('returns profile', () => {
|
||||
const { result } = renderHook(() => useGetProfile());
|
||||
const email = result.current.user?.email;
|
||||
expect(email).toEqual(result.current.user?.email);
|
||||
expect(useQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
@ -1,33 +0,0 @@
|
||||
import { ApolloError, useQuery } from '@apollo/client';
|
||||
import { gql } from 'graphql-tag';
|
||||
import { User } from '../../interfaces/user.interface';
|
||||
import { useGetUserEmailFromToken } from '../AuthenticationHooks';
|
||||
|
||||
const GET_USER_PROFILE = gql`
|
||||
query GetUserProfile($email: String!) {
|
||||
users(where: { email: { _eq: $email } }, limit: 1) {
|
||||
id
|
||||
email
|
||||
first_name
|
||||
last_name
|
||||
tenant {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type ProfileResult = {
|
||||
loading: boolean;
|
||||
error?: ApolloError;
|
||||
user?: User;
|
||||
};
|
||||
|
||||
export const useGetProfile = (): ProfileResult => {
|
||||
const email = useGetUserEmailFromToken();
|
||||
const { loading, error, data } = useQuery(GET_USER_PROFILE, {
|
||||
variables: { email },
|
||||
});
|
||||
return { loading, error, user: data?.users[0] };
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useQuery, QueryResult } from '@apollo/client';
|
||||
import { useGetTenantByDomain } from '../useGetTenantByDomain';
|
||||
|
||||
jest.mock('@apollo/client', () => ({
|
||||
useQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useGetTenantByDomain', () => {
|
||||
beforeEach(() => {
|
||||
const result: Partial<QueryResult<any>> = {
|
||||
data: { tenants: [{ domain: 'pilot.twenty.com' }] },
|
||||
loading: false,
|
||||
error: undefined,
|
||||
};
|
||||
(useQuery as jest.Mock).mockImplementation(() => result as QueryResult);
|
||||
});
|
||||
|
||||
it('returns tenant by domain', () => {
|
||||
const { result } = renderHook(() => useGetTenantByDomain());
|
||||
const domain = result.current.tenant?.domain;
|
||||
expect(domain).toEqual(result.current.tenant?.domain);
|
||||
expect(useQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
@ -1,31 +0,0 @@
|
||||
import { ApolloError, useQuery } from '@apollo/client';
|
||||
import { gql } from 'graphql-tag';
|
||||
import { Tenant } from '../../interfaces/tenant.interface';
|
||||
|
||||
const GET_TENANT_BY_DOMAIN = gql`
|
||||
query GetTenantByDomain($domain: String!) {
|
||||
tenants(where: { domain: { _eq: $domain } }, limit: 1) {
|
||||
auth0_client_id
|
||||
domain
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type TenantResult = {
|
||||
loading: boolean;
|
||||
error?: ApolloError;
|
||||
tenant?: Tenant;
|
||||
};
|
||||
|
||||
export const useGetTenantByDomain = (): TenantResult => {
|
||||
const domain = window.location.hostname;
|
||||
const { loading, error, data } = useQuery(GET_TENANT_BY_DOMAIN, {
|
||||
variables: { domain },
|
||||
context: {
|
||||
headers: {
|
||||
'x-hasura-default-role': 'public',
|
||||
},
|
||||
},
|
||||
});
|
||||
return { loading, error, tenant: data?.tenants[0] };
|
||||
};
|
@ -14,17 +14,8 @@ import { setContext } from '@apollo/client/link/context';
|
||||
const httpLink = createHttpLink({ uri: process.env.REACT_APP_API_URL });
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
const requestHeaders = { ...headers };
|
||||
const token = localStorage.getItem('accessToken');
|
||||
const headerContainsPublicRole =
|
||||
requestHeaders.hasOwnProperty('x-hasura-default-role') &&
|
||||
requestHeaders['x-hasura-default-role'] === 'public';
|
||||
if (!headerContainsPublicRole && token) {
|
||||
requestHeaders['authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers: requestHeaders,
|
||||
headers: headers,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
export interface TokenPayload {
|
||||
'https://hasura.io/jwt/claims': {
|
||||
'x-hasura-user-email': string;
|
||||
'x-hasura-user-id': string;
|
||||
};
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export interface Tenant {
|
||||
id: number;
|
||||
name: string;
|
||||
domain: string;
|
||||
auth0_client_id: string;
|
||||
}
|
@ -1,9 +1,6 @@
|
||||
import { Tenant } from './tenant.interface';
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
tenant?: Tenant;
|
||||
}
|
||||
|
@ -29,12 +29,6 @@ const StyledEmail = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledTenant = styled.div`
|
||||
display: flex;
|
||||
text-transform: capitalize;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const StyledAvatar = styled.div`
|
||||
display: flex;
|
||||
width: 40px;
|
||||
@ -61,7 +55,6 @@ function ProfileContainer({ user }: OwnProps) {
|
||||
</StyledAvatar>
|
||||
<StyledInfoContainer>
|
||||
<StyledEmail>{user?.email}</StyledEmail>
|
||||
<StyledTenant>{user?.tenant?.name}</StyledTenant>
|
||||
</StyledInfoContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
@ -15,12 +15,6 @@ export const NavbarOnInsights = () => (
|
||||
email: 'charles@twenty.com',
|
||||
first_name: 'Charles',
|
||||
last_name: 'Bochet',
|
||||
tenant: {
|
||||
id: 1,
|
||||
name: 'Twenty',
|
||||
domain: 'pilot.twenty.com',
|
||||
auth0_client_id: 'auth0_client_id',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
|
@ -1,16 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useGetAccessToken } from '../hooks/AuthenticationHooks';
|
||||
|
||||
function AuthCallback() {
|
||||
const { token } = useGetAccessToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
export default AuthCallback;
|
@ -1,8 +1 @@
|
||||
HASURA_GRAPHQL_JWT_SECRET=REPLACE_ME
|
||||
HASURA_GRAPHQL_ADMIN_SECRET=hasura_secret
|
||||
|
||||
REACT_APP_AUTH0_DOMAIN=twenty-dev.eu.auth0.com
|
||||
REACT_APP_AUTH0_CLIENT_ID=REPLACE_ME
|
||||
REACT_APP_AUTH0_CALLBACK_URL=/auth/callback
|
||||
REACT_APP_AUTH0_AUDIENCE=hasura-dev
|
||||
REACT_APP_API_URL=http://localhost:8080/v1/graphql
|
||||
REACT_APP_API_URL=http://localhost:3000
|
||||
|
@ -2,38 +2,16 @@ up: ##
|
||||
@docker-compose up -d
|
||||
|
||||
logs: ##
|
||||
@docker-compose logs $(container) -f
|
||||
@docker-compose logs twenty -f
|
||||
|
||||
api-sh: ##
|
||||
@docker-compose exec twenty-api bash
|
||||
|
||||
api-console: ##
|
||||
@docker-compose exec twenty-api bash -c "cd /api; \
|
||||
socat TCP-LISTEN:8080,fork TCP:twenty-api:8080 & \
|
||||
socat TCP-LISTEN:9695,fork,reuseaddr,bind=twenty-api TCP:127.0.0.1:9695 & \
|
||||
socat TCP-LISTEN:9693,fork,reuseaddr,bind=twenty-api TCP:127.0.0.1:9693 & \
|
||||
hasura console --log-level DEBUG --address "127.0.0.1" --no-browser || exit 1"
|
||||
|
||||
api-make-seed: ##
|
||||
@docker-compose exec twenty-api bash -c "cd /api; \
|
||||
hasura seeds create seed --from-table $(table)"
|
||||
|
||||
api-make-migrations: ##
|
||||
@docker-compose exec twenty-api bash -c "cd /api; \
|
||||
hasura migrate create $(name)"
|
||||
|
||||
api-make-metadata: ##
|
||||
@docker-compose exec twenty-api bash -c "cd /api; \
|
||||
hasura metadata export"
|
||||
|
||||
front-sh: ##
|
||||
@docker-compose exec twenty-front sh
|
||||
sh: ##
|
||||
@docker-compose exec twenty sh
|
||||
|
||||
front-test: ##
|
||||
@docker-compose exec twenty-front sh -c "npm run test"
|
||||
@docker-compose exec twenty sh -c "npm run test"
|
||||
|
||||
front-coverage: ##
|
||||
@docker-compose exec twenty-front sh -c "npm run coverage"
|
||||
@docker-compose exec twenty sh -c "npm run coverage"
|
||||
|
||||
front-storybook: ##
|
||||
@docker-compose exec twenty-front sh -c "npm run storybook"
|
||||
@docker-compose exec twenty sh -c "npm run storybook"
|
||||
|
@ -1,45 +1,19 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
twenty-front:
|
||||
twenty:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: ./infra/dev/twenty-front/Dockerfile
|
||||
dockerfile: ./infra/dev/twenty/Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3001:3001"
|
||||
- "6006:6006"
|
||||
volumes:
|
||||
- ../../front:/app/front
|
||||
- ../../server:/app/server
|
||||
- ../../infra:/app/infra
|
||||
depends_on:
|
||||
- postgres
|
||||
twenty-server:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: ./infra/dev/twenty-server/Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- postgres
|
||||
twenty-api:
|
||||
build: ./twenty-api
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "9695:9695"
|
||||
- "9693:9693"
|
||||
volumes:
|
||||
- ../../api:/api
|
||||
depends_on:
|
||||
- "postgres"
|
||||
restart: always
|
||||
environment:
|
||||
HASURA_GRAPHQL_METADATA_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/hasura
|
||||
HASURA_GRAPHQL_PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/twenty
|
||||
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
|
||||
HASURA_GRAPHQL_DEV_MODE: "true"
|
||||
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: "public"
|
||||
HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET}
|
||||
HASURA_GRAPHQL_JWT_SECRET: ${HASURA_GRAPHQL_JWT_SECRET}
|
||||
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
|
||||
postgres:
|
||||
build: ./postgres
|
||||
volumes:
|
||||
|
@ -1,2 +1 @@
|
||||
CREATE DATABASE twenty;
|
||||
CREATE DATABASE hasura;
|
||||
CREATE DATABASE twenty;
|
@ -1,9 +0,0 @@
|
||||
FROM hasura/graphql-engine:latest as api
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y curl
|
||||
RUN apt-get install -y socat
|
||||
RUN apt-get install -y vim
|
||||
RUN curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
|
||||
|
||||
CMD ["sh", "-c", "graphql-engine serve"]
|
@ -1,10 +0,0 @@
|
||||
FROM node:18-alpine as app
|
||||
|
||||
WORKDIR /app
|
||||
COPY ../.. .
|
||||
|
||||
WORKDIR /app/server
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
CMD ["npm", "run", "start:prod"]
|
@ -5,6 +5,10 @@ COPY ../.. .
|
||||
|
||||
WORKDIR /app/front
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
WORKDIR /app/server
|
||||
RUN npm install
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["npm", "run", "start"]
|
@ -1,7 +0,0 @@
|
||||
FROM hasura/graphql-engine:latest as api
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y curl
|
||||
RUN curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
|
||||
|
||||
CMD ["sh", "-c", "graphql-engine serve"]
|
@ -1,10 +1,6 @@
|
||||
FROM node:18-alpine as app
|
||||
|
||||
ARG REACT_APP_API_URL
|
||||
ARG REACT_APP_AUTH0_DOMAIN
|
||||
ARG REACT_APP_AUTH0_CLIENT_ID
|
||||
ARG REACT_APP_AUTH0_CALLBACK_URL
|
||||
ARG REACT_APP_AUTH0_AUDIENCE
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
503
package-lock.json
generated
Normal file
503
package-lock.json
generated
Normal file
@ -0,0 +1,503 @@
|
||||
{
|
||||
"name": "twenty",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "twenty",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"concurrently": "^7.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz",
|
||||
"integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"date-fns": "^2.29.1",
|
||||
"lodash": "^4.17.21",
|
||||
"rxjs": "^7.0.0",
|
||||
"shell-quote": "^1.7.3",
|
||||
"spawn-command": "^0.0.2-1",
|
||||
"supports-color": "^8.1.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"yargs": "^17.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
|
||||
"integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz",
|
||||
"integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/spawn-command": {
|
||||
"version": "0.0.2-1",
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
|
||||
"integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg=="
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz",
|
||||
"integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"requires": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"concurrently": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz",
|
||||
"integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==",
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"date-fns": "^2.29.1",
|
||||
"lodash": "^4.17.21",
|
||||
"rxjs": "^7.0.0",
|
||||
"shell-quote": "^1.7.3",
|
||||
"spawn-command": "^0.0.2-1",
|
||||
"supports-color": "^8.1.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"yargs": "^17.3.1"
|
||||
}
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
|
||||
"integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"shell-quote": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz",
|
||||
"integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ=="
|
||||
},
|
||||
"spawn-command": {
|
||||
"version": "0.0.2-1",
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
|
||||
"integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "17.7.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz",
|
||||
"integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
|
||||
"requires": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
|
||||
}
|
||||
}
|
||||
}
|
16
package.json
Normal file
16
package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "twenty",
|
||||
"version": "1.0.0",
|
||||
"description": "Open-source CRM",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"server": "npm start --prefix server",
|
||||
"front": "npm start --prefix front",
|
||||
"start": "concurrently \"npm run server\" \"npm run front\""
|
||||
},
|
||||
"author": "Charles Bochet",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"concurrently": "^7.6.0"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user