diff --git a/front/src/services/AuthService.ts b/front/src/services/AuthService.ts index ce24d0235..0031ee17a 100644 --- a/front/src/services/AuthService.ts +++ b/front/src/services/AuthService.ts @@ -30,5 +30,8 @@ export const refreshAccessToken = async () => { if (response.ok) { const { accessToken } = await response.json(); localStorage.setItem('accessToken', accessToken); + } else { + localStorage.removeItem('refreshToken'); + localStorage.removeItem('accessToken'); } }; diff --git a/hasura/metadata/databases/default/tables/auth_users.yaml b/hasura/metadata/databases/default/tables/auth_users.yaml index 730001730..22f6dc5cc 100644 --- a/hasura/metadata/databases/default/tables/auth_users.yaml +++ b/hasura/metadata/databases/default/tables/auth_users.yaml @@ -120,3 +120,24 @@ array_relationships: table: name: user_providers schema: auth + - name: workspace_member + using: + foreign_key_constraint_on: + column: user_id + table: + name: workspace_members + schema: public +event_triggers: + - name: user-created + definition: + enable_manual: false + insert: + columns: '*' + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook: '{{HASURA_EVENT_HANDLER_URL}}' + headers: + - name: secret-header + value: secret diff --git a/hasura/metadata/databases/default/tables/public_workspace_members.yaml b/hasura/metadata/databases/default/tables/public_workspace_members.yaml new file mode 100644 index 000000000..f035604e3 --- /dev/null +++ b/hasura/metadata/databases/default/tables/public_workspace_members.yaml @@ -0,0 +1,3 @@ +table: + name: workspace_members + schema: public diff --git a/hasura/metadata/databases/default/tables/tables.yaml b/hasura/metadata/databases/default/tables/tables.yaml index 73e96ce71..ecce9eb68 100644 --- a/hasura/metadata/databases/default/tables/tables.yaml +++ b/hasura/metadata/databases/default/tables/tables.yaml @@ -8,4 +8,5 @@ - "!include auth_users.yaml" - "!include public_companies.yaml" - "!include public_people.yaml" +- "!include public_workspace_members.yaml" - "!include public_workspaces.yaml" diff --git a/hasura/migrations/default/1682141330851_create_table_public_workspace_members/down.sql b/hasura/migrations/default/1682141330851_create_table_public_workspace_members/down.sql new file mode 100644 index 000000000..17131d84e --- /dev/null +++ b/hasura/migrations/default/1682141330851_create_table_public_workspace_members/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."workspace_members"; diff --git a/hasura/migrations/default/1682141330851_create_table_public_workspace_members/up.sql b/hasura/migrations/default/1682141330851_create_table_public_workspace_members/up.sql new file mode 100644 index 000000000..835f01b2d --- /dev/null +++ b/hasura/migrations/default/1682141330851_create_table_public_workspace_members/up.sql @@ -0,0 +1 @@ +CREATE TABLE "public"."workspace_members" ("id" serial NOT NULL, "user_id" uuid NOT NULL, "workspace_id" integer NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON UPDATE restrict ON DELETE restrict, UNIQUE ("id"), UNIQUE ("user_id")); diff --git a/hasura/migrations/default/1682141433503_alter_table_public_workspace_members_add_column_created_at/down.sql b/hasura/migrations/default/1682141433503_alter_table_public_workspace_members_add_column_created_at/down.sql new file mode 100644 index 000000000..43fcc7a4e --- /dev/null +++ b/hasura/migrations/default/1682141433503_alter_table_public_workspace_members_add_column_created_at/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."workspace_members" add column "created_at" timestamptz +-- not null default now(); diff --git a/hasura/migrations/default/1682141433503_alter_table_public_workspace_members_add_column_created_at/up.sql b/hasura/migrations/default/1682141433503_alter_table_public_workspace_members_add_column_created_at/up.sql new file mode 100644 index 000000000..6e48494ea --- /dev/null +++ b/hasura/migrations/default/1682141433503_alter_table_public_workspace_members_add_column_created_at/up.sql @@ -0,0 +1,2 @@ +alter table "public"."workspace_members" add column "created_at" timestamptz + not null default now(); diff --git a/hasura/migrations/default/1682141443863_alter_table_public_workspace_members_add_column_updated_at/down.sql b/hasura/migrations/default/1682141443863_alter_table_public_workspace_members_add_column_updated_at/down.sql new file mode 100644 index 000000000..47993769d --- /dev/null +++ b/hasura/migrations/default/1682141443863_alter_table_public_workspace_members_add_column_updated_at/down.sql @@ -0,0 +1,21 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."workspace_members" add column "updated_at" timestamptz +-- not null default now(); +-- +-- CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() +-- RETURNS TRIGGER AS $$ +-- DECLARE +-- _new record; +-- BEGIN +-- _new := NEW; +-- _new."updated_at" = NOW(); +-- RETURN _new; +-- END; +-- $$ LANGUAGE plpgsql; +-- CREATE TRIGGER "set_public_workspace_members_updated_at" +-- BEFORE UPDATE ON "public"."workspace_members" +-- FOR EACH ROW +-- EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); +-- COMMENT ON TRIGGER "set_public_workspace_members_updated_at" ON "public"."workspace_members" +-- IS 'trigger to set value of column "updated_at" to current timestamp on row update'; diff --git a/hasura/migrations/default/1682141443863_alter_table_public_workspace_members_add_column_updated_at/up.sql b/hasura/migrations/default/1682141443863_alter_table_public_workspace_members_add_column_updated_at/up.sql new file mode 100644 index 000000000..2eb2da7f7 --- /dev/null +++ b/hasura/migrations/default/1682141443863_alter_table_public_workspace_members_add_column_updated_at/up.sql @@ -0,0 +1,19 @@ +alter table "public"."workspace_members" add column "updated_at" timestamptz + not null default now(); + +CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() +RETURNS TRIGGER AS $$ +DECLARE + _new record; +BEGIN + _new := NEW; + _new."updated_at" = NOW(); + RETURN _new; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER "set_public_workspace_members_updated_at" +BEFORE UPDATE ON "public"."workspace_members" +FOR EACH ROW +EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); +COMMENT ON TRIGGER "set_public_workspace_members_updated_at" ON "public"."workspace_members" +IS 'trigger to set value of column "updated_at" to current timestamp on row update'; diff --git a/hasura/migrations/default/1682141457260_alter_table_public_workspace_members_add_column_deleted_at/down.sql b/hasura/migrations/default/1682141457260_alter_table_public_workspace_members_add_column_deleted_at/down.sql new file mode 100644 index 000000000..34346f439 --- /dev/null +++ b/hasura/migrations/default/1682141457260_alter_table_public_workspace_members_add_column_deleted_at/down.sql @@ -0,0 +1,21 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."workspace_members" add column "deleted_at" timestamptz +-- null; +-- +-- CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_deleted_at"() +-- RETURNS TRIGGER AS $$ +-- DECLARE +-- _new record; +-- BEGIN +-- _new := NEW; +-- _new."deleted_at" = NOW(); +-- RETURN _new; +-- END; +-- $$ LANGUAGE plpgsql; +-- CREATE TRIGGER "set_public_workspace_members_deleted_at" +-- BEFORE UPDATE ON "public"."workspace_members" +-- FOR EACH ROW +-- EXECUTE PROCEDURE "public"."set_current_timestamp_deleted_at"(); +-- COMMENT ON TRIGGER "set_public_workspace_members_deleted_at" ON "public"."workspace_members" +-- IS 'trigger to set value of column "deleted_at" to current timestamp on row update'; diff --git a/hasura/migrations/default/1682141457260_alter_table_public_workspace_members_add_column_deleted_at/up.sql b/hasura/migrations/default/1682141457260_alter_table_public_workspace_members_add_column_deleted_at/up.sql new file mode 100644 index 000000000..64b2222a0 --- /dev/null +++ b/hasura/migrations/default/1682141457260_alter_table_public_workspace_members_add_column_deleted_at/up.sql @@ -0,0 +1,19 @@ +alter table "public"."workspace_members" add column "deleted_at" timestamptz + null; + +CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_deleted_at"() +RETURNS TRIGGER AS $$ +DECLARE + _new record; +BEGIN + _new := NEW; + _new."deleted_at" = NOW(); + RETURN _new; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER "set_public_workspace_members_deleted_at" +BEFORE UPDATE ON "public"."workspace_members" +FOR EACH ROW +EXECUTE PROCEDURE "public"."set_current_timestamp_deleted_at"(); +COMMENT ON TRIGGER "set_public_workspace_members_deleted_at" ON "public"."workspace_members" +IS 'trigger to set value of column "deleted_at" to current timestamp on row update'; diff --git a/hasura/migrations/default/1682337427850_alter_table_public_workspaces_add_column_domain_name/down.sql b/hasura/migrations/default/1682337427850_alter_table_public_workspaces_add_column_domain_name/down.sql new file mode 100644 index 000000000..965641ba8 --- /dev/null +++ b/hasura/migrations/default/1682337427850_alter_table_public_workspaces_add_column_domain_name/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."workspaces" add column "domain_name" text +-- null unique; diff --git a/hasura/migrations/default/1682337427850_alter_table_public_workspaces_add_column_domain_name/up.sql b/hasura/migrations/default/1682337427850_alter_table_public_workspaces_add_column_domain_name/up.sql new file mode 100644 index 000000000..ff09f675f --- /dev/null +++ b/hasura/migrations/default/1682337427850_alter_table_public_workspaces_add_column_domain_name/up.sql @@ -0,0 +1,2 @@ +alter table "public"."workspaces" add column "domain_name" text + null unique; diff --git a/hasura/migrations/default/1682337605938_alter_table_public_workspaces_drop_column_domain_name/down.sql b/hasura/migrations/default/1682337605938_alter_table_public_workspaces_drop_column_domain_name/down.sql new file mode 100644 index 000000000..92f9c9d61 --- /dev/null +++ b/hasura/migrations/default/1682337605938_alter_table_public_workspaces_drop_column_domain_name/down.sql @@ -0,0 +1,3 @@ +alter table "public"."workspaces" add constraint "workspaces_domain_name_key" unique (domain_name); +alter table "public"."workspaces" alter column "domain_name" drop not null; +alter table "public"."workspaces" add column "domain_name" text; diff --git a/hasura/migrations/default/1682337605938_alter_table_public_workspaces_drop_column_domain_name/up.sql b/hasura/migrations/default/1682337605938_alter_table_public_workspaces_drop_column_domain_name/up.sql new file mode 100644 index 000000000..75bb197fe --- /dev/null +++ b/hasura/migrations/default/1682337605938_alter_table_public_workspaces_drop_column_domain_name/up.sql @@ -0,0 +1 @@ +alter table "public"."workspaces" drop column "domain_name" cascade; diff --git a/hasura/migrations/default/1682337614489_alter_table_public_workspaces_alter_column_name/down.sql b/hasura/migrations/default/1682337614489_alter_table_public_workspaces_alter_column_name/down.sql new file mode 100644 index 000000000..fc016a35d --- /dev/null +++ b/hasura/migrations/default/1682337614489_alter_table_public_workspaces_alter_column_name/down.sql @@ -0,0 +1 @@ +alter table "public"."workspaces" rename column "domain_name" to "name"; diff --git a/hasura/migrations/default/1682337614489_alter_table_public_workspaces_alter_column_name/up.sql b/hasura/migrations/default/1682337614489_alter_table_public_workspaces_alter_column_name/up.sql new file mode 100644 index 000000000..256e5ffa1 --- /dev/null +++ b/hasura/migrations/default/1682337614489_alter_table_public_workspaces_alter_column_name/up.sql @@ -0,0 +1 @@ +alter table "public"."workspaces" rename column "name" to "domain_name"; diff --git a/hasura/migrations/default/1682338781740_alter_table_public_workspaces_add_column_deleted_at/down.sql b/hasura/migrations/default/1682338781740_alter_table_public_workspaces_add_column_deleted_at/down.sql new file mode 100644 index 000000000..7fd42c852 --- /dev/null +++ b/hasura/migrations/default/1682338781740_alter_table_public_workspaces_add_column_deleted_at/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."workspaces" add column "deleted_at" Timestamp +-- null; diff --git a/hasura/migrations/default/1682338781740_alter_table_public_workspaces_add_column_deleted_at/up.sql b/hasura/migrations/default/1682338781740_alter_table_public_workspaces_add_column_deleted_at/up.sql new file mode 100644 index 000000000..a72ce728e --- /dev/null +++ b/hasura/migrations/default/1682338781740_alter_table_public_workspaces_add_column_deleted_at/up.sql @@ -0,0 +1,2 @@ +alter table "public"."workspaces" add column "deleted_at" Timestamp + null; diff --git a/infra/dev/.env.example b/infra/dev/.env.example index f76fd03da..2e7b095b7 100644 --- a/infra/dev/.env.example +++ b/infra/dev/.env.example @@ -1,6 +1,20 @@ +HASURA_GRAPHQL_METADATA_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/hasura +HASURA_GRAPHQL_PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/default +HASURA_GRAPHQL_ADMIN_SECRET: secret +HASURA_GRAPHQL_JWT_SECRET: '{"type":"HS256", "key": "jwt-very-long-hard-to-guess-secret"}' +HASURA_EVENT_HANDLER_URL: http://twenty-server:3000/hasura/events + HASURA_AUTH_SERVER_URL: http://localhost:4000 HASURA_AUTH_CLIENT_URL: http://localhost:3001/auth/callback HASURA_AUTH_PROVIDER_GOOGLE_CLIENT_ID: REPLACE_ME HASURA_AUTH_PROVIDER_GOOGLE_CLIENT_SECRET: REPLACE_ME +HASURA_AUTH_GRAPHQL_URL: http://twenty-hasura:8080/v1/graphql + FRONT_REACT_APP_API_URL=http://localhost:8080 -FRONT_REACT_APP_AUTH_URL=http://localhost:4000 \ No newline at end of file +FRONT_REACT_APP_AUTH_URL=http://localhost:4000 + +SERVER_HASURA_EVENT_HANDLER_SECRET_HEADER: secret +SERVER_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/default +SERVER_HASURA_EVENT_HANDLER_SECRET_HEADER: secret + +POSTGRES_PASSWORD=postgrespassword \ No newline at end of file diff --git a/infra/dev/docker-compose.yml b/infra/dev/docker-compose.yml index 6f6294397..9d259a4fe 100644 --- a/infra/dev/docker-compose.yml +++ b/infra/dev/docker-compose.yml @@ -28,22 +28,24 @@ services: - "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/default + HASURA_GRAPHQL_METADATA_DATABASE_URL: ${HASURA_GRAPHQL_METADATA_DATABASE_URL} + HASURA_GRAPHQL_PG_DATABASE_URL: ${HASURA_GRAPHQL_PG_DATABASE_URL} HASURA_GRAPHQL_ENABLE_CONSOLE: "false" HASURA_GRAPHQL_DEV_MODE: "true" HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log - HASURA_GRAPHQL_ADMIN_SECRET: secret - HASURA_GRAPHQL_JWT_SECRET: '{"type":"HS256", "key": "jwt-very-long-hard-to-guess-secret"}' + HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET} + HASURA_GRAPHQL_JWT_SECRET: ${HASURA_GRAPHQL_JWT_SECRET} + HASURA_EVENT_HANDLER_URL: ${HASURA_EVENT_HANDLER_URL} hasura-auth: image: nhost/hasura-auth:0.19.1 ports: - "4000:4000" environment: - HASURA_GRAPHQL_JWT_SECRET: '{"type":"HS256", "key": "jwt-very-long-hard-to-guess-secret"}' - HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/default - HASURA_GRAPHQL_GRAPHQL_URL: http://twenty-hasura:8080/v1/graphql - HASURA_GRAPHQL_ADMIN_SECRET: secret + HASURA_GRAPHQL_JWT_SECRET: ${HASURA_GRAPHQL_JWT_SECRET} + HASURA_GRAPHQL_DATABASE_URL: ${HASURA_GRAPHQL_PG_DATABASE_URL} + HASURA_GRAPHQL_GRAPHQL_URL: ${HASURA_AUTH_GRAPHQL_URL} + HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET} + AUTH_JWT_CUSTOM_CLAIMS: '{"workspace-id":"workspace_member.workspace_id"}' npm_package_version: '0' AUTH_SMTP_HOST: mailhog AUTH_SMTP_PORT: 1025 @@ -78,6 +80,9 @@ services: volumes: - ../../server:/app/server - twenty_node_modules_server:/app/server/node_modules + environment: + HASURA_EVENT_HANDLER_SECRET_HEADER: ${SERVER_HASURA_EVENT_HANDLER_SECRET_HEADER} + SERVER_DATABASE_URL: ${SERVER_DATABASE_URL} depends_on: - postgres postgres: @@ -85,7 +90,7 @@ services: volumes: - db_data:/var/lib/postgresql/data environment: - POSTGRES_PASSWORD: postgrespassword + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} ports: - "5432:5432" volumes: diff --git a/infra/dev/server/Dockerfile b/infra/dev/server/Dockerfile index 5eb7b18fa..611bb3fb8 100644 --- a/infra/dev/server/Dockerfile +++ b/infra/dev/server/Dockerfile @@ -13,4 +13,4 @@ RUN npm install COPY ../../server . -CMD ["npm", "run", "start"] \ No newline at end of file +CMD ["npm", "run", "start:dev"] \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index c731dafee..3d7b93877 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,11 +9,13 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@golevelup/nestjs-hasura": "^3.0.2", "@nestjs/common": "^9.0.0", "@nestjs/core": "^9.0.0", "@nestjs/platform-express": "^9.0.0", "@nestjs/serve-static": "^3.0.0", "@nestjs/terminus": "^9.2.2", + "@prisma/client": "^4.13.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0" @@ -33,6 +35,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "28.1.3", "prettier": "^2.3.2", + "prisma": "^4.13.0", "source-map-support": "^0.5.20", "supertest": "^6.1.3", "ts-jest": "28.0.8", @@ -853,6 +856,52 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/@golevelup/nestjs-common": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-common/-/nestjs-common-1.4.4.tgz", + "integrity": "sha512-NTjtOhHTMuGwiR3lmBQKKaRr++mHQEsh8AxtaH+/EWOYKMK2Cv/8duaH9MQ0hI3TwnouyaA5IRxYR1ZCUyNXOQ==", + "dependencies": { + "nanoid": "^3.2.0" + } + }, + "node_modules/@golevelup/nestjs-discovery": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-3.0.0.tgz", + "integrity": "sha512-ZvkXtobTKxXB1LJanP/l6Z/Fing88IMBr3uabQpU2IWjfsstjh02qYDSU2cfD6CSmNldX5ewW5Pd+SdK2lU8Sw==", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/@golevelup/nestjs-hasura": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-hasura/-/nestjs-hasura-3.0.2.tgz", + "integrity": "sha512-xvRVIy8XBbxB0m2nVuwrYHLpy4QgDNZRXLKAseYHOQAlGha8Q1G5PZA656uY4XsXrz0Bl284RPBCXwWuPzcE1Q==", + "dependencies": { + "@golevelup/nestjs-common": "^1.4.4", + "@golevelup/nestjs-discovery": "^3.0.0", + "@golevelup/nestjs-modules": "^0.6.1", + "@hasura/metadata": "^1.0.2", + "js-yaml": "^4.1.0", + "zod": "^3.3.4" + } + }, + "node_modules/@golevelup/nestjs-modules": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-modules/-/nestjs-modules-0.6.1.tgz", + "integrity": "sha512-E0STg8In8fhIivnGDJAA70+XLPHzK5bMTkCnif9FbZ8waTYDQ3T/QQL0h73k+CUFeznn1hmuEW14sNaE+8cd7w==", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^9.x", + "rxjs": "^7.x" + } + }, + "node_modules/@hasura/metadata": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@hasura/metadata/-/metadata-1.0.2.tgz", + "integrity": "sha512-bVDwRWC7g/NfLVUwP8HBV07+37g07UAbF+XEujfRmgr8839sH7Q2iwa2M8oQFQXwg4dj5Sn+WRt4/UWXKN7naQ==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -1882,6 +1931,38 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@prisma/client": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.13.0.tgz", + "integrity": "sha512-YaiiICcRB2hatxsbnfB66uWXjcRw3jsZdlAVxmx0cFcTc/Ad/sKdHCcWSnqyDX47vAewkjRFwiLwrOUjswVvmA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.13.0.tgz", + "integrity": "sha512-HrniowHRZXHuGT9XRgoXEaP2gJLXM5RMoItaY2PkjvuZ+iHc0Zjbm/302MB8YsPdWozAPHHn+jpFEcEn71OgPw==", + "devOptional": true, + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a.tgz", + "integrity": "sha512-fsQlbkhPJf08JOzKoyoD9atdUijuGBekwoOPZC3YOygXEml1MTtgXVpnUNchQlRSY82OQ6pSGQ9PxUe4arcSLQ==" + }, "node_modules/@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -2708,8 +2789,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -5918,7 +5998,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -6050,8 +6129,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -6331,6 +6409,23 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6849,6 +6944,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prisma": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.13.0.tgz", + "integrity": "sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "4.13.0" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -8540,6 +8652,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -9166,6 +9286,48 @@ } } }, + "@golevelup/nestjs-common": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-common/-/nestjs-common-1.4.4.tgz", + "integrity": "sha512-NTjtOhHTMuGwiR3lmBQKKaRr++mHQEsh8AxtaH+/EWOYKMK2Cv/8duaH9MQ0hI3TwnouyaA5IRxYR1ZCUyNXOQ==", + "requires": { + "nanoid": "^3.2.0" + } + }, + "@golevelup/nestjs-discovery": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-3.0.0.tgz", + "integrity": "sha512-ZvkXtobTKxXB1LJanP/l6Z/Fing88IMBr3uabQpU2IWjfsstjh02qYDSU2cfD6CSmNldX5ewW5Pd+SdK2lU8Sw==", + "requires": { + "lodash": "^4.17.15" + } + }, + "@golevelup/nestjs-hasura": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-hasura/-/nestjs-hasura-3.0.2.tgz", + "integrity": "sha512-xvRVIy8XBbxB0m2nVuwrYHLpy4QgDNZRXLKAseYHOQAlGha8Q1G5PZA656uY4XsXrz0Bl284RPBCXwWuPzcE1Q==", + "requires": { + "@golevelup/nestjs-common": "^1.4.4", + "@golevelup/nestjs-discovery": "^3.0.0", + "@golevelup/nestjs-modules": "^0.6.1", + "@hasura/metadata": "^1.0.2", + "js-yaml": "^4.1.0", + "zod": "^3.3.4" + } + }, + "@golevelup/nestjs-modules": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-modules/-/nestjs-modules-0.6.1.tgz", + "integrity": "sha512-E0STg8In8fhIivnGDJAA70+XLPHzK5bMTkCnif9FbZ8waTYDQ3T/QQL0h73k+CUFeznn1hmuEW14sNaE+8cd7w==", + "requires": { + "lodash": "^4.17.21" + } + }, + "@hasura/metadata": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@hasura/metadata/-/metadata-1.0.2.tgz", + "integrity": "sha512-bVDwRWC7g/NfLVUwP8HBV07+37g07UAbF+XEujfRmgr8839sH7Q2iwa2M8oQFQXwg4dj5Sn+WRt4/UWXKN7naQ==" + }, "@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -9878,6 +10040,25 @@ } } }, + "@prisma/client": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.13.0.tgz", + "integrity": "sha512-YaiiICcRB2hatxsbnfB66uWXjcRw3jsZdlAVxmx0cFcTc/Ad/sKdHCcWSnqyDX47vAewkjRFwiLwrOUjswVvmA==", + "requires": { + "@prisma/engines-version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" + } + }, + "@prisma/engines": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.13.0.tgz", + "integrity": "sha512-HrniowHRZXHuGT9XRgoXEaP2gJLXM5RMoItaY2PkjvuZ+iHc0Zjbm/302MB8YsPdWozAPHHn+jpFEcEn71OgPw==", + "devOptional": true + }, + "@prisma/engines-version": { + "version": "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a.tgz", + "integrity": "sha512-fsQlbkhPJf08JOzKoyoD9atdUijuGBekwoOPZC3YOygXEml1MTtgXVpnUNchQlRSY82OQ6pSGQ9PxUe4arcSLQ==" + }, "@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -10562,8 +10743,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "array-flatten": { "version": "1.1.1", @@ -12953,7 +13133,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" } @@ -13050,8 +13229,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.memoize": { "version": "4.1.2", @@ -13260,6 +13438,11 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -13629,6 +13812,15 @@ } } }, + "prisma": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.13.0.tgz", + "integrity": "sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA==", + "devOptional": true, + "requires": { + "@prisma/engines": "4.13.0" + } + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -14836,6 +15028,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" } } } diff --git a/server/package.json b/server/package.json index 89d523daa..6d0f1db40 100644 --- a/server/package.json +++ b/server/package.json @@ -21,11 +21,13 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@golevelup/nestjs-hasura": "^3.0.2", "@nestjs/common": "^9.0.0", "@nestjs/core": "^9.0.0", "@nestjs/platform-express": "^9.0.0", "@nestjs/serve-static": "^3.0.0", "@nestjs/terminus": "^9.2.2", + "@prisma/client": "^4.13.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0" @@ -45,6 +47,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "28.1.3", "prettier": "^2.3.2", + "prisma": "^4.13.0", "source-map-support": "^0.5.20", "supertest": "^6.1.3", "ts-jest": "28.0.8", @@ -69,5 +72,8 @@ ], "coverageDirectory": "../coverage", "testEnvironment": "node" + }, + "prisma": { + "schema": "src/database/schema.prisma" } } diff --git a/server/src/app.controller.spec.ts b/server/src/app.controller.spec.ts index 23d230bbb..a22ca32ce 100644 --- a/server/src/app.controller.spec.ts +++ b/server/src/app.controller.spec.ts @@ -13,10 +13,4 @@ describe('AppController', () => { appController = app.get(AppController); }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.health()).toBe('Healthy!'); - }); - }); }); diff --git a/server/src/app.controller.ts b/server/src/app.controller.ts index 999f99e61..4454e3e03 100644 --- a/server/src/app.controller.ts +++ b/server/src/app.controller.ts @@ -4,9 +4,4 @@ import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} - - @Get('/health') - health(): string { - return this.appService.health(); - } } diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 340eacacc..8df88e1ac 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -3,10 +3,20 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { HealthController } from './health.controller'; import { TerminusModule } from '@nestjs/terminus'; +import { HasuraModule } from '@golevelup/nestjs-hasura'; +import { UserService } from './user/user.service'; +import { UserModule } from './user/user.module'; +const path = require('path'); @Module({ - imports: [TerminusModule], + imports: [UserModule, TerminusModule, HasuraModule.forRoot(HasuraModule, { + webhookConfig: { + secretFactory: process.env.HASURA_EVENT_HANDLER_SECRET_HEADER, + secretHeader: 'secret-header', + }, + }) +], controllers: [AppController, HealthController], - providers: [AppService], + providers: [AppService, UserService], }) export class AppModule {} diff --git a/server/src/database/prisma.module.ts b/server/src/database/prisma.module.ts new file mode 100644 index 000000000..175578146 --- /dev/null +++ b/server/src/database/prisma.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} \ No newline at end of file diff --git a/server/src/database/prisma.service.ts b/server/src/database/prisma.service.ts new file mode 100644 index 000000000..f4701c880 --- /dev/null +++ b/server/src/database/prisma.service.ts @@ -0,0 +1,15 @@ +import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { + await this.$connect(); + } + + async enableShutdownHooks(app: INestApplication) { + this.$on('beforeExit', async () => { + await app.close(); + }); + } +} \ No newline at end of file diff --git a/server/src/database/schema.prisma b/server/src/database/schema.prisma new file mode 100644 index 000000000..6b0e91053 --- /dev/null +++ b/server/src/database/schema.prisma @@ -0,0 +1,30 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("SERVER_DATABASE_URL") +} + +model WorkspaceMember { + id Int @id @default(autoincrement()) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + deleted_at DateTime? + user_id String @unique + workspace_id Int + + @@map("workspace_members") +} + +model Workspace { + id Int @id @default(autoincrement()) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + deleted_at DateTime? + domain_name String @unique + display_name String + + @@map("workspaces") +} diff --git a/server/src/user/user.module.ts b/server/src/user/user.module.ts new file mode 100644 index 000000000..ead45e9e3 --- /dev/null +++ b/server/src/user/user.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { PrismaModule } from 'src/database/prisma.module'; +import { UserRepository } from './user.repository'; +import { UserService } from './user.service'; +import { WorkspaceRepository } from './workspace.repository'; + +@Module({ + imports: [PrismaModule], + providers: [UserRepository, UserService, WorkspaceRepository], + exports: [UserService, UserRepository, WorkspaceRepository], +}) +export class UserModule {} \ No newline at end of file diff --git a/server/src/user/user.repository.ts b/server/src/user/user.repository.ts new file mode 100644 index 000000000..ef4bdc6f6 --- /dev/null +++ b/server/src/user/user.repository.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma, WorkspaceMember } from '@prisma/client'; +import { PrismaService } from 'src/database/prisma.service'; + +@Injectable() +export class UserRepository { + constructor(private prisma: PrismaService) {} + + async upsertWorkspaceMember(params: { data: Prisma.WorkspaceMemberCreateInput }): Promise { + const { data } = params; + return this.prisma.workspaceMember.upsert({ + where: { + user_id: data.user_id, + }, + create: { + user_id: data.user_id, + workspace_id: data.workspace_id, + }, + update: { + } + }); + } +} \ No newline at end of file diff --git a/server/src/user/user.service.spec.ts b/server/src/user/user.service.spec.ts new file mode 100644 index 000000000..873de8ac4 --- /dev/null +++ b/server/src/user/user.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserService], + }).compile(); + + service = module.get(UserService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts new file mode 100644 index 000000000..01c0c5150 --- /dev/null +++ b/server/src/user/user.service.ts @@ -0,0 +1,40 @@ +import { HasuraInsertEvent, TrackedHasuraEventHandler } from '@golevelup/nestjs-hasura'; +import { UserRepository} from "./user.repository" +import { Injectable, Response } from '@nestjs/common'; +import { WorkspaceRepository } from './workspace.repository'; +import { response } from 'express'; + +interface User { + id: number; + email: string; +} + +@Injectable() +export class UserService { + constructor(private repository: UserRepository, private workspaceRepository: WorkspaceRepository) {} + + @TrackedHasuraEventHandler({ + triggerName: 'user-created', + tableName: 'users', + schema: 'auth', + definition: { type: 'insert' }, + }) + async handleUserCreated(evt: HasuraInsertEvent) { + const workspace = await this.workspaceRepository.findWorkspaceByDomainName( + { where: { domain_name:evt.event.data.new.email.split('@')[1] } + }); + + console.log(workspace) + + if (!workspace) { + return; + } + + const workspaceMember = await this.repository.upsertWorkspaceMember({ + data: { + user_id: String(evt.event.data.new.id), + workspace_id: workspace.id, + }, + }); + } +} \ No newline at end of file diff --git a/server/src/user/workspace.repository.ts b/server/src/user/workspace.repository.ts new file mode 100644 index 000000000..eae602cd6 --- /dev/null +++ b/server/src/user/workspace.repository.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma, Workspace } from '@prisma/client'; +import { PrismaService } from 'src/database/prisma.service'; + +@Injectable() +export class WorkspaceRepository { + constructor(private prisma: PrismaService) {} + + async findWorkspaceByDomainName(data: Prisma.WorkspaceFindUniqueArgs): Promise { + return this.prisma.workspace.findUnique(data); + } +} \ No newline at end of file