From 1260595b736a2f104078bb4b5d98dd6b96d8cb38 Mon Sep 17 00:00:00 2001 From: Paulo Queiroz <16908491+raggesilver@users.noreply.github.com> Date: Mon, 20 Jan 2025 18:19:30 -0300 Subject: [PATCH 1/3] Sync --- app/components/workspace/plan-settings.vue | 43 + app/components/workspace/settings.vue | 4 +- app/composables/useWorkspacePlan.ts | 24 + drizzle/0025_plain_gwen_stacy.sql | 8 + drizzle/meta/0025_snapshot.json | 1196 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + nuxt.config.ts | 3 + package.json | 3 +- pnpm-lock.yaml | 179 ++- server/db/schema.ts | 21 + server/routes/api/workspace/[id]/plan.get.ts | 23 + server/services/plan.ts | 30 + server/services/stripe.ts | 33 + server/utils/cache.ts | 8 +- server/utils/stripe.ts | 13 + 15 files changed, 1577 insertions(+), 18 deletions(-) create mode 100644 app/components/workspace/plan-settings.vue create mode 100644 app/composables/useWorkspacePlan.ts create mode 100644 drizzle/0025_plain_gwen_stacy.sql create mode 100644 drizzle/meta/0025_snapshot.json create mode 100644 server/routes/api/workspace/[id]/plan.get.ts create mode 100644 server/services/plan.ts create mode 100644 server/services/stripe.ts create mode 100644 server/utils/stripe.ts diff --git a/app/components/workspace/plan-settings.vue b/app/components/workspace/plan-settings.vue new file mode 100644 index 0000000..bd59f2f --- /dev/null +++ b/app/components/workspace/plan-settings.vue @@ -0,0 +1,43 @@ + + + diff --git a/app/components/workspace/settings.vue b/app/components/workspace/settings.vue index ea1b627..e5f02c7 100644 --- a/app/components/workspace/settings.vue +++ b/app/components/workspace/settings.vue @@ -2,6 +2,7 @@ import { LazyWorkspaceGeneralSettings, LazyWorkspaceMemberSettings, + LazyWorkspacePlanSettings, LazyWorkspaceRuleSettings, LazyWorkspaceUsage, } from "#components"; @@ -11,7 +12,7 @@ defineProps<{ workspace: Workspace; }>(); -const validPages = ["general", "members", "rules", "usage"] as const; +const validPages = ["general", "members", "rules", "usage", "billing"] as const; type ValidPage = (typeof validPages)[number]; const components = { @@ -19,6 +20,7 @@ const components = { members: LazyWorkspaceMemberSettings, rules: LazyWorkspaceRuleSettings, usage: LazyWorkspaceUsage, + billing: LazyWorkspacePlanSettings, }; const page = useQueryParam("settings", { diff --git a/app/composables/useWorkspacePlan.ts b/app/composables/useWorkspacePlan.ts new file mode 100644 index 0000000..0f85d92 --- /dev/null +++ b/app/composables/useWorkspacePlan.ts @@ -0,0 +1,24 @@ +import { queryOptions } from "@tanstack/vue-query"; +import type { Plan } from "~~/server/db/schema"; + +export const getWorkspacePlanOptions = ( + workspaceId: MaybeRefOrGetter, +) => + queryOptions({ + queryKey: ["workspace", workspaceId, "plan"], + }); + +export const useWorkspacePlan = (workspaceId: MaybeRefOrGetter) => { + const client = useQueryClient(); + + return useQuery( + { + queryKey: getWorkspacePlanOptions(workspaceId).queryKey, + queryFn: async () => + useRequestFetch()(`/api/workspace/${toValue(workspaceId)}/plan`).then( + (response) => response.plan, + ), + }, + client, + ); +}; diff --git a/drizzle/0025_plain_gwen_stacy.sql b/drizzle/0025_plain_gwen_stacy.sql new file mode 100644 index 0000000..a6e97fe --- /dev/null +++ b/drizzle/0025_plain_gwen_stacy.sql @@ -0,0 +1,8 @@ +CREATE TABLE "plans" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" varchar(255) NOT NULL, + "subscription_id" varchar(255) NOT NULL, + "workspace_id" uuid NOT NULL +); +--> statement-breakpoint +ALTER TABLE "plans" ADD CONSTRAINT "plans_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/meta/0025_snapshot.json b/drizzle/meta/0025_snapshot.json new file mode 100644 index 0000000..f9ca6ab --- /dev/null +++ b/drizzle/meta/0025_snapshot.json @@ -0,0 +1,1196 @@ +{ + "id": "081a9e77-b49e-41f3-8f69-54ea92ec33c8", + "prevId": "f6efec83-d368-4852-b188-f2132337bf7e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.assignees": { + "name": "assignees", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "assignees_task_id_tasks_id_fk": { + "name": "assignees_task_id_tasks_id_fk", + "tableFrom": "assignees", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assignees_user_id_users_id_fk": { + "name": "assignees_user_id_users_id_fk", + "tableFrom": "assignees", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "assignees_task_id_user_id_pk": { + "name": "assignees_task_id_user_id_pk", + "columns": [ + "task_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.attachments": { + "name": "attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "board_id": { + "name": "board_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "attachments_board_id_boards_id_fk": { + "name": "attachments_board_id_boards_id_fk", + "tableFrom": "attachments", + "tableTo": "boards", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "attachments_task_id_tasks_id_fk": { + "name": "attachments_task_id_tasks_id_fk", + "tableFrom": "attachments", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "attachments_uploaded_by_users_id_fk": { + "name": "attachments_uploaded_by_users_id_fk", + "tableFrom": "attachments", + "tableTo": "users", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.boards": { + "name": "boards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "boards_workspace_id_name_index": { + "name": "boards_workspace_id_name_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "boards_owner_id_users_id_fk": { + "name": "boards_owner_id_users_id_fk", + "tableFrom": "boards", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "boards_workspace_id_workspaces_id_fk": { + "name": "boards_workspace_id_workspaces_id_fk", + "tableFrom": "boards", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collaborators": { + "name": "collaborators", + "schema": "", + "columns": { + "board_id": { + "name": "board_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "collaborators_board_id_user_id_index": { + "name": "collaborators_board_id_user_id_index", + "columns": [ + { + "expression": "board_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collaborators_board_id_boards_id_fk": { + "name": "collaborators_board_id_boards_id_fk", + "tableFrom": "collaborators", + "tableTo": "boards", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collaborators_user_id_users_id_fk": { + "name": "collaborators_user_id_users_id_fk", + "tableFrom": "collaborators", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "collaborators_board_id_user_id_pk": { + "name": "collaborators_board_id_user_id_pk", + "columns": [ + "board_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_links": { + "name": "invitation_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "board_id": { + "name": "board_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_links_workspace_id_active_index": { + "name": "invitation_links_workspace_id_active_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation_links\".\"active\" = true AND \"invitation_links\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_links_board_id_active_index": { + "name": "invitation_links_board_id_active_index", + "columns": [ + { + "expression": "board_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation_links\".\"active\" = true AND \"invitation_links\".\"board_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_links_workspace_id_workspaces_id_fk": { + "name": "invitation_links_workspace_id_workspaces_id_fk", + "tableFrom": "invitation_links", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_links_board_id_boards_id_fk": { + "name": "invitation_links_board_id_boards_id_fk", + "tableFrom": "invitation_links", + "tableTo": "boards", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "valid_invitation_target": { + "name": "valid_invitation_target", + "value": "(\n (\"invitation_links\".\"workspace_id\" IS NULL AND \"invitation_links\".\"board_id\" IS NOT NULL) OR \n (\"invitation_links\".\"workspace_id\" IS NOT NULL AND \"invitation_links\".\"board_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "labels_board_id_boards_id_fk": { + "name": "labels_board_id_boards_id_fk", + "tableFrom": "labels", + "tableTo": "boards", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth": { + "name": "oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id_in_provider": { + "name": "user_id_in_provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_provider_user_id_index": { + "name": "oauth_provider_user_id_index", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_user_id_users_id_fk": { + "name": "oauth_user_id_users_id_fk", + "tableFrom": "oauth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plans": { + "name": "plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "plans_workspace_id_workspaces_id_fk": { + "name": "plans_workspace_id_workspaces_id_fk", + "tableFrom": "plans", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.status_columns": { + "name": "status_columns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_updated_by": { + "name": "last_updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "status_columns_board_id_name_index": { + "name": "status_columns_board_id_name_index", + "columns": [ + { + "expression": "board_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "status_columns_workspace_id_workspaces_id_fk": { + "name": "status_columns_workspace_id_workspaces_id_fk", + "tableFrom": "status_columns", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_columns_board_id_boards_id_fk": { + "name": "status_columns_board_id_boards_id_fk", + "tableFrom": "status_columns", + "tableTo": "boards", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_columns_created_by_users_id_fk": { + "name": "status_columns_created_by_users_id_fk", + "tableFrom": "status_columns", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_columns_last_updated_by_users_id_fk": { + "name": "status_columns_last_updated_by_users_id_fk", + "tableFrom": "status_columns", + "tableTo": "users", + "columnsFrom": [ + "last_updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_labels": { + "name": "task_labels", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "task_labels_task_id_tasks_id_fk": { + "name": "task_labels_task_id_tasks_id_fk", + "tableFrom": "task_labels", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_labels_label_id_labels_id_fk": { + "name": "task_labels_label_id_labels_id_fk", + "tableFrom": "task_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_labels_task_id_label_id_pk": { + "name": "task_labels_task_id_label_id_pk", + "columns": [ + "task_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "board_id": { + "name": "board_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status_column_id": { + "name": "status_column_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_updated_by": { + "name": "last_updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tasks_board_id_boards_id_fk": { + "name": "tasks_board_id_boards_id_fk", + "tableFrom": "tasks", + "tableTo": "boards", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_status_column_id_status_columns_id_fk": { + "name": "tasks_status_column_id_status_columns_id_fk", + "tableFrom": "tasks", + "tableTo": "status_columns", + "columnsFrom": [ + "status_column_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_created_by_users_id_fk": { + "name": "tasks_created_by_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_last_updated_by_users_id_fk": { + "name": "tasks_last_updated_by_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "last_updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "full_name": { + "name": "full_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_picture_url": { + "name": "profile_picture_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_email_index": { + "name": "users_email_index", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_collaborators": { + "name": "workspace_collaborators", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_collaborators_workspace_id_workspaces_id_fk": { + "name": "workspace_collaborators_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_collaborators", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_collaborators_user_id_users_id_fk": { + "name": "workspace_collaborators_user_id_users_id_fk", + "tableFrom": "workspace_collaborators", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_collaborators_workspace_id_user_id_pk": { + "name": "workspace_collaborators_workspace_id_user_id_pk", + "columns": [ + "workspace_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspaces_owner_id_users_id_fk": { + "name": "workspaces_owner_id_users_id_fk", + "tableFrom": "workspaces", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 8a0ce08..20760b3 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -176,6 +176,13 @@ "when": 1732737304772, "tag": "0024_dark_gorgon", "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1736426745818, + "tag": "0025_plain_gwen_stacy", + "breakpoints": true } ] } \ No newline at end of file diff --git a/nuxt.config.ts b/nuxt.config.ts index 18d34ca..ead074e 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -40,6 +40,9 @@ export default defineNuxtConfig({ redis: { url: "", }, + stripe: { + secretKey: "", + }, }, // nitro: { diff --git a/package.json b/package.json index 3491cad..52ffd40 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "radix-vue": "^1.9.12", "shadcn-nuxt": "^0.10.4", "slug": "^10.0.0", + "stripe": "^17.5.0", "tailwind-merge": "^2.6.0", "vaul-vue": "^0.2.0", "vee-validate": "^4.15.0", @@ -76,5 +77,5 @@ "vitest": "^2.1.8", "vue-tsc": "^2.2.0" }, - "packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321" + "packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 544786c..1a9c8d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: slug: specifier: ^10.0.0 version: 10.0.0 + stripe: + specifier: ^17.5.0 + version: 17.5.0 tailwind-merge: specifier: ^2.6.0 version: 2.6.0 @@ -2487,9 +2490,6 @@ packages: '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} - '@types/node@22.10.2': - resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} - '@types/node@22.10.4': resolution: {integrity: sha512-99l6wv4HEzBQhvaU/UGoeBoCK61SCROQaCCGyQSgX2tEQ3rKkNZ2S7CEWnS/4s1LV+8ODdK21UeyR1fHP2mXug==} @@ -3195,6 +3195,14 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.1: + resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + engines: {node: '>= 0.4'} + + call-bound@1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -3749,6 +3757,10 @@ packages: drizzle-orm: '>=0.36.0' zod: '>=3.0.0' + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -3820,9 +3832,21 @@ packages: errx@0.1.0: resolution: {integrity: sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -4176,9 +4200,17 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.2.7: + resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + engines: {node: '>= 0.4'} + get-port-please@3.1.2: resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stdin@8.0.0: resolution: {integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==} engines: {node: '>=10'} @@ -4257,6 +4289,10 @@ packages: resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -4282,6 +4318,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -4756,6 +4796,10 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.1: resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} @@ -5168,6 +5212,10 @@ packages: oauth4webapi@3.1.4: resolution: {integrity: sha512-eVfN3nZNbok2s/ROifO0UAc5G8nRoLSbrcKJ09OqmucgnhXEfdIQOR4gq1eJH1rN3gV7rNw62bDEgftsgFtBEg==} + object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + object-treeify@1.1.33: resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} engines: {node: '>= 10'} @@ -5651,6 +5699,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.13.1: + resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5937,6 +5989,22 @@ packages: shimmer@1.2.1: resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -6104,6 +6172,10 @@ packages: strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + stripe@17.5.0: + resolution: {integrity: sha512-kcyeAkDFjGsVl17FqnG7q/+xIjt0ZjOo9Dm+q8deAvs2Xe4iAHrhxyoP4etUVFc+/LZJANjIPVR+ZOnt9hr/Ug==} + engines: {node: '>=12.*'} + strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} @@ -9853,7 +9925,7 @@ snapshots: '@types/connect@3.4.36': dependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.4 '@types/debug@4.1.12': dependencies: @@ -9879,7 +9951,7 @@ snapshots: '@types/http-proxy@1.17.15': dependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.4 '@types/json-schema@7.0.15': {} @@ -9891,11 +9963,7 @@ snapshots: '@types/mysql@2.15.26': dependencies: - '@types/node': 22.10.2 - - '@types/node@22.10.2': - dependencies: - undici-types: 6.20.0 + '@types/node': 22.10.4 '@types/node@22.10.4': dependencies: @@ -9911,7 +9979,7 @@ snapshots: '@types/pg@8.6.1': dependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.4 pg-protocol: 1.7.0 pg-types: 2.2.0 @@ -9925,7 +9993,7 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.4 '@types/unist@2.0.11': {} @@ -10958,6 +11026,16 @@ snapshots: cac@6.7.14: {} + call-bind-apply-helpers@1.0.1: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.3: + dependencies: + call-bind-apply-helpers: 1.0.1 + get-intrinsic: 1.2.7 + callsites@3.1.0: {} caniuse-api@3.0.0: @@ -11374,6 +11452,12 @@ snapshots: drizzle-orm: 0.38.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(postgres@3.4.5) zod: 3.24.1 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -11444,8 +11528,16 @@ snapshots: errx@0.1.0: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.6.0: {} + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + esbuild-register@3.6.0(esbuild@0.19.12): dependencies: debug: 4.4.0(supports-color@9.4.0) @@ -11981,8 +12073,26 @@ snapshots: get-caller-file@2.0.5: {} + get-intrinsic@1.2.7: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-port-please@3.1.2: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.0.0 + get-stdin@8.0.0: {} get-stream@6.0.1: {} @@ -12071,6 +12181,8 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.1.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -12103,6 +12215,8 @@ snapshots: has-flag@4.0.0: {} + has-symbols@1.1.0: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -12631,6 +12745,8 @@ snapshots: markdown-table@3.0.4: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.1: dependencies: '@types/mdast': 4.0.4 @@ -13383,6 +13499,8 @@ snapshots: oauth4webapi@3.1.4: {} + object-inspect@1.13.3: {} + object-treeify@1.1.33: {} ofetch@1.4.1: @@ -13872,6 +13990,10 @@ snapshots: punycode@2.3.1: {} + qs@6.13.1: + dependencies: + side-channel: 1.1.0 + queue-microtask@1.2.3: {} queue-tick@1.0.1: {} @@ -14290,6 +14412,34 @@ snapshots: shimmer@1.2.1: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -14467,6 +14617,11 @@ snapshots: dependencies: js-tokens: 9.0.1 + stripe@17.5.0: + dependencies: + '@types/node': 22.10.4 + qs: 6.13.1 + strnum@1.0.5: {} style-value-types@5.1.2: diff --git a/server/db/schema.ts b/server/db/schema.ts index f326c9b..cea3d4f 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -43,6 +43,9 @@ export const workspaces = pgTable("workspaces", (t) => ({ .uuid("owner_id") .notNull() .references(() => users.id), + + // Each workspace is its own customer in Stripe. + customerId: t.varchar("customer_id", { length: 255 }), createdAt: t.timestamp("created_at").notNull().defaultNow(), updatedAt: t.timestamp("updated_at").notNull().defaultNow(), })); @@ -296,6 +299,24 @@ export type InvitationLink = _InvitationLink & ); export type NewInvitationLink = typeof invitationLinks.$inferInsert; +export const plans = pgTable("plans", (t) => ({ + id: t.uuid("id").primaryKey().defaultRandom(), + name: t.varchar("name", { length: 255 }).notNull(), + // We store the price in Stripe only to avoid out-of-sync issues. + + // Subscription id in Stripe. + subscriptionId: t.varchar("subscription_id", { length: 255 }).notNull(), + // FIXME: onDelete: "cascade" could be problematic if we delete a workspace + // without canceling the subscription first. + workspaceId: t + .uuid("workspace_id") + .notNull() + .references(() => workspaces.id, { onDelete: "cascade" }), +})); + +export type Plan = typeof plans.$inferSelect; +export type NewPlan = typeof plans.$inferInsert; + // Relations export const oauthRelations = relations(oauth, ({ one }) => ({ diff --git a/server/routes/api/workspace/[id]/plan.get.ts b/server/routes/api/workspace/[id]/plan.get.ts new file mode 100644 index 0000000..8617341 --- /dev/null +++ b/server/routes/api/workspace/[id]/plan.get.ts @@ -0,0 +1,23 @@ +import { validateId } from "~/lib/validation"; +import { isUserWorkspaceOwner } from "~~/server/services/authorization"; +import { planService } from "~~/server/services/plan"; + +export default defineEventHandler(async (event) => { + const { user } = await requireUserSession(event); + + const { id } = await getValidatedRouterParams( + event, + validateId("id").parseAsync, + ); + + if (false === (await isUserWorkspaceOwner(user.id, id))) { + throw createError({ + status: 403, + statusMessage: "You are not the owner of this workspace.", + }); + } + + const plan = await planService.getPlanForWorkspace(id); + + return { plan }; +}); diff --git a/server/services/plan.ts b/server/services/plan.ts new file mode 100644 index 0000000..2ca2ea8 --- /dev/null +++ b/server/services/plan.ts @@ -0,0 +1,30 @@ +import { db } from "../db/db"; +import { plans, type NewPlan, type Plan } from "../db/schema"; + +class PlanService { + @cacheResult(60 * 60 * 24) + async getPlanForWorkspace(workspaceId: string): Promise { + return db.query.plans + .findFirst({ where: (t, { eq }) => eq(t.workspaceId, workspaceId) }) + .execute() + .then((plan) => plan ?? null); + } + + async setPlanForWorkspace( + workspaceId: string, + plan: Omit, + ): Promise { + const [_plan] = await db + .insert(plans) + .values({ ...plan, workspaceId }) + .onConflictDoNothing() + .returning() + .execute(); + + invalidateCache(getCacheKey(this.getPlanForWorkspace, [workspaceId])); + + return _plan; + } +} + +export const planService = new PlanService(); diff --git a/server/services/stripe.ts b/server/services/stripe.ts new file mode 100644 index 0000000..85190b9 --- /dev/null +++ b/server/services/stripe.ts @@ -0,0 +1,33 @@ +// Ideally we would have an interface called PaymentService that would be an +// abstraction for all payment services. This would allow for easier swapping of +// payment services in the future or even supporting multiple payment services +// at once. Our architecture, however, is highly coupled to Stripe, so there's +// no point in abstracting anything. + +import Stripe from "stripe"; +import type { Workspace } from "~~/server/db/schema"; + +class StripeService { + stripe: Stripe; + + constructor() { + const config = useRuntimeConfig(); + + this.stripe = new Stripe(config.stripe.secretKey, { + apiVersion: "2024-12-18.acacia", + }); + } + + async createCustomerForWorkspace(workspace: Workspace): Promise { + const customer = await this.stripe.customers.create({ + name: workspace.name, + metadata: { + workspaceId: workspace.id, + }, + }); + + return customer.id; + } +} + +export const stripeService = new StripeService(); diff --git a/server/utils/cache.ts b/server/utils/cache.ts index 68b2e75..b85a393 100644 --- a/server/utils/cache.ts +++ b/server/utils/cache.ts @@ -39,10 +39,10 @@ async function invalidateCache( * @param args The arguments passed to the function * @returns A string key combining the function name and arguments */ -function getCacheKey unknown>( - fn: F, - args: Parameters, -): string { +function getCacheKey< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + F extends (...args: any[]) => any, +>(fn: F, args: Parameters): string { return `${fn.name}_${args.join("_")}`; } diff --git a/server/utils/stripe.ts b/server/utils/stripe.ts new file mode 100644 index 0000000..4be2cbf --- /dev/null +++ b/server/utils/stripe.ts @@ -0,0 +1,13 @@ +import Stripe from "stripe"; + +let _stripe: Stripe | null = null; + +export const getStripe = () => { + if (!_stripe) { + const config = useRuntimeConfig(); + _stripe = new Stripe(config.stripe.secretKey, { + apiVersion: "2024-12-18.acacia", + }); + } + return _stripe; +}; From 39ad2477860b2cb702217b47b62743fa3fdd42f7 Mon Sep 17 00:00:00 2001 From: Paulo Queiroz <16908491+raggesilver@users.noreply.github.com> Date: Tue, 29 Jul 2025 01:16:44 -0300 Subject: [PATCH 2/3] Initial work on board filters - Migrate deps, move to Nuxt 4 - Add migration because at some point we added customer_id? - Create useBoardFilters composable and begin implementing assignee filtering --- .../board-collaborator-list/index.vue | 17 +- app/components/mini-task.vue | 7 +- app/components/status-column.vue | 15 +- app/composables/useBoardFilters.ts | 67 + drizzle/0026_foamy_micromacro.sql | 1 + drizzle/meta/0026_snapshot.json | 1202 ++ drizzle/meta/_journal.json | 7 + package.json | 100 +- patches/@hebilicious__vue-query-nuxt.patch | 39 + pnpm-lock.yaml | 11613 +++++++++------- pnpm-workspace.yaml | 8 + server/routes/api/invitation/accept.get.ts | 1 + server/routes/api/task/[id]/attachment.put.ts | 6 + .../collaborator/[collaborator].delete.ts | 2 + auth.d.ts => shared/auth.d.ts | 5 + 15 files changed, 7847 insertions(+), 5243 deletions(-) create mode 100644 app/composables/useBoardFilters.ts create mode 100644 drizzle/0026_foamy_micromacro.sql create mode 100644 drizzle/meta/0026_snapshot.json create mode 100644 patches/@hebilicious__vue-query-nuxt.patch create mode 100644 pnpm-workspace.yaml rename auth.d.ts => shared/auth.d.ts (73%) diff --git a/app/components/board-collaborator-list/index.vue b/app/components/board-collaborator-list/index.vue index 2643940..cf51e5a 100644 --- a/app/components/board-collaborator-list/index.vue +++ b/app/components/board-collaborator-list/index.vue @@ -10,12 +10,11 @@ const props = withDefaults( */ ownerFirst?: boolean; /** - * If provided, the list will render at most `limit` collaborators. If - * there are more than `limit` collaborators, the `limit`-th element on the - * list will be a placeholder for the number of remaining collaborators. + * If provided, the list will render at most `limit` collaborators. If there + * are more than `limit` collaborators, the `limit`-th element on the list + * will be a placeholder for the number of remaining collaborators. * * If this number is less than 1, the list will render all collaborators. - * */ limit?: number; }>(), @@ -64,6 +63,8 @@ const collapsedCollaboratorsText = computed(() => .join("\n") : "", ); + +const { toggleAssignee, filters } = useBoardFilters(); diff --git a/app/components/mini-task.vue b/app/components/mini-task.vue index 94d1e13..ce86288 100644 --- a/app/components/mini-task.vue +++ b/app/components/mini-task.vue @@ -7,6 +7,7 @@ const props = defineProps<{ onDragStart?: (event: DragEvent, task: Task) => void; }>(); +const route = useRoute(); const { labels } = inject(BOARD_DATA_KEY)!; const isDragging = ref(false); @@ -36,6 +37,10 @@ const labelMap = computed(() => { }, {}) ?? {} ); }); + +const toLink = computed(() => { + return { query: { ...route.query, "view-task": props.task.id } }; +});