From c0e06ac8ba6997c1e8d0647feb00a1d391b25d1d Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Wed, 28 Jan 2026 21:34:26 +0000 Subject: [PATCH] User.email and implementation --- .../drizzle/0027_volatile_otto_octavius.sql | 4 + .../backend/drizzle/meta/0027_snapshot.json | 1159 +++++++++++++++++ packages/backend/drizzle/meta/_journal.json | 7 + packages/backend/scripts/db-seed.ts | 16 +- packages/backend/src/db/queries/users.ts | 11 +- packages/backend/src/routes/auth/register.ts | 14 +- .../subscription/create-checkout-session.ts | 6 +- .../frontend/src/components/login-form.tsx | 16 +- packages/shared/src/api-schemas.ts | 2 + packages/shared/src/constants.ts | 1 + packages/shared/src/index.ts | 1 + packages/shared/src/schema.ts | 2 + 12 files changed, 1220 insertions(+), 19 deletions(-) create mode 100644 packages/backend/drizzle/0027_volatile_otto_octavius.sql create mode 100644 packages/backend/drizzle/meta/0027_snapshot.json diff --git a/packages/backend/drizzle/0027_volatile_otto_octavius.sql b/packages/backend/drizzle/0027_volatile_otto_octavius.sql new file mode 100644 index 0000000..aaa2d87 --- /dev/null +++ b/packages/backend/drizzle/0027_volatile_otto_octavius.sql @@ -0,0 +1,4 @@ +ALTER TABLE "User" ADD COLUMN "email" varchar(255);--> statement-breakpoint +UPDATE "User" SET "email" = 'user_' || id || '@placeholder.local' WHERE "email" IS NULL;--> statement-breakpoint +ALTER TABLE "User" ALTER COLUMN "email" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "User" ADD CONSTRAINT "User_email_unique" UNIQUE("email"); \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0027_snapshot.json b/packages/backend/drizzle/meta/0027_snapshot.json new file mode 100644 index 0000000..ba821f3 --- /dev/null +++ b/packages/backend/drizzle/meta/0027_snapshot.json @@ -0,0 +1,1159 @@ +{ + "id": "b826ec09-e4ac-49b1-9975-b36f5be69b0b", + "prevId": "9104baeb-85d7-4fdb-87cc-9d5ac6b85ec8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Issue": { + "name": "Issue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Issue_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'Task'" + }, + "status": { + "name": "status", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'TO DO'" + }, + "title": { + "name": "title", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true + }, + "creatorId": { + "name": "creatorId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sprintId": { + "name": "sprintId", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_project_issue_number": { + "name": "unique_project_issue_number", + "columns": [ + { + "expression": "projectId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Issue_projectId_Project_id_fk": { + "name": "Issue_projectId_Project_id_fk", + "tableFrom": "Issue", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Issue_creatorId_User_id_fk": { + "name": "Issue_creatorId_User_id_fk", + "tableFrom": "Issue", + "tableTo": "User", + "columnsFrom": [ + "creatorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Issue_sprintId_Sprint_id_fk": { + "name": "Issue_sprintId_Sprint_id_fk", + "tableFrom": "Issue", + "tableTo": "Sprint", + "columnsFrom": [ + "sprintId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IssueAssignee": { + "name": "IssueAssignee", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "IssueAssignee_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "issueId": { + "name": "issueId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assignedAt": { + "name": "assignedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_issue_user": { + "name": "unique_issue_user", + "columns": [ + { + "expression": "issueId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "IssueAssignee_issueId_Issue_id_fk": { + "name": "IssueAssignee_issueId_Issue_id_fk", + "tableFrom": "IssueAssignee", + "tableTo": "Issue", + "columnsFrom": [ + "issueId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "IssueAssignee_userId_User_id_fk": { + "name": "IssueAssignee_userId_User_id_fk", + "tableFrom": "IssueAssignee", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IssueComment": { + "name": "IssueComment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "IssueComment_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "issueId": { + "name": "issueId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "IssueComment_issueId_Issue_id_fk": { + "name": "IssueComment_issueId_Issue_id_fk", + "tableFrom": "IssueComment", + "tableTo": "Issue", + "columnsFrom": [ + "issueId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "IssueComment_userId_User_id_fk": { + "name": "IssueComment_userId_User_id_fk", + "tableFrom": "IssueComment", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Organisation": { + "name": "Organisation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Organisation_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "iconURL": { + "name": "iconURL", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "statuses": { + "name": "statuses", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"TO DO\":\"#fafafa\",\"IN PROGRESS\":\"#f97316\",\"REVIEW\":\"#8952bc\",\"DONE\":\"#22c55e\",\"REJECTED\":\"#ef4444\",\"ARCHIVED\":\"#a1a1a1\",\"MERGED\":\"#a1a1a1\"}'::json" + }, + "issueTypes": { + "name": "issueTypes", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"Task\":{\"icon\":\"checkBox\",\"color\":\"#e4bd47\"},\"Bug\":{\"icon\":\"bug\",\"color\":\"#ef4444\"}}'::json" + }, + "features": { + "name": "features", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"userAvatars\":true,\"issueTypes\":true,\"issueStatus\":true,\"issueDescriptions\":true,\"issueTimeTracking\":true,\"issueAssignees\":true,\"issueAssigneesShownInTable\":true,\"issueCreator\":true,\"issueComments\":true,\"sprints\":true}'::json" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Organisation_slug_unique": { + "name": "Organisation_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OrganisationMember": { + "name": "OrganisationMember", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "OrganisationMember_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "organisationId": { + "name": "organisationId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "OrganisationMember_organisationId_Organisation_id_fk": { + "name": "OrganisationMember_organisationId_Organisation_id_fk", + "tableFrom": "OrganisationMember", + "tableTo": "Organisation", + "columnsFrom": [ + "organisationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "OrganisationMember_userId_User_id_fk": { + "name": "OrganisationMember_userId_User_id_fk", + "tableFrom": "OrganisationMember", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Payment": { + "name": "Payment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Payment_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "subscriptionId": { + "name": "subscriptionId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "stripePaymentIntentId": { + "name": "stripePaymentIntentId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'gbp'" + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Payment_subscriptionId_Subscription_id_fk": { + "name": "Payment_subscriptionId_Subscription_id_fk", + "tableFrom": "Payment", + "tableTo": "Subscription", + "columnsFrom": [ + "subscriptionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Project": { + "name": "Project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Project_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "key": { + "name": "key", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "organisationId": { + "name": "organisationId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "creatorId": { + "name": "creatorId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Project_organisationId_Organisation_id_fk": { + "name": "Project_organisationId_Organisation_id_fk", + "tableFrom": "Project", + "tableTo": "Organisation", + "columnsFrom": [ + "organisationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Project_creatorId_User_id_fk": { + "name": "Project_creatorId_User_id_fk", + "tableFrom": "Project", + "tableTo": "User", + "columnsFrom": [ + "creatorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Session": { + "name": "Session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Session_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "csrfToken": { + "name": "csrfToken", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Session_userId_User_id_fk": { + "name": "Session_userId_User_id_fk", + "tableFrom": "Session", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Sprint": { + "name": "Sprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Sprint_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#a1a1a1'" + }, + "startDate": { + "name": "startDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "endDate": { + "name": "endDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Sprint_projectId_Project_id_fk": { + "name": "Sprint_projectId_Project_id_fk", + "tableFrom": "Sprint", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Subscription": { + "name": "Subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Subscription_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionItemId": { + "name": "stripeSubscriptionItemId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripePriceId": { + "name": "stripePriceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'incomplete'" + }, + "currentPeriodStart": { + "name": "currentPeriodStart", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "currentPeriodEnd": { + "name": "currentPeriodEnd", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelAtPeriodEnd": { + "name": "cancelAtPeriodEnd", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trialEnd": { + "name": "trialEnd", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Subscription_userId_User_id_fk": { + "name": "Subscription_userId_User_id_fk", + "tableFrom": "Subscription", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TimedSession": { + "name": "TimedSession", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "TimedSession_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issueId": { + "name": "issueId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timestamps": { + "name": "timestamps", + "type": "timestamp[]", + "primaryKey": false, + "notNull": true + }, + "endedAt": { + "name": "endedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "TimedSession_userId_User_id_fk": { + "name": "TimedSession_userId_User_id_fk", + "tableFrom": "TimedSession", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "TimedSession_issueId_Issue_id_fk": { + "name": "TimedSession_issueId_Issue_id_fk", + "tableFrom": "TimedSession", + "tableTo": "Issue", + "columnsFrom": [ + "issueId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "User_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "passwordHash": { + "name": "passwordHash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatarURL": { + "name": "avatarURL", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "iconPreference": { + "name": "iconPreference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'pixel'" + }, + "plan": { + "name": "plan", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "User_username_unique": { + "name": "User_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "User_email_unique": { + "name": "User_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/backend/drizzle/meta/_journal.json b/packages/backend/drizzle/meta/_journal.json index baed3b1..7ee5a8f 100644 --- a/packages/backend/drizzle/meta/_journal.json +++ b/packages/backend/drizzle/meta/_journal.json @@ -190,6 +190,13 @@ "when": 1769615487574, "tag": "0026_stale_shocker", "breakpoints": true + }, + { + "idx": 27, + "version": "7", + "when": 1769635016079, + "tag": "0027_volatile_otto_octavius", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/backend/scripts/db-seed.ts b/packages/backend/scripts/db-seed.ts index 56ad15e..ccb0ee2 100644 --- a/packages/backend/scripts/db-seed.ts +++ b/packages/backend/scripts/db-seed.ts @@ -97,15 +97,15 @@ const issueComments = [ const passwordHash = await hashPassword("a"); const users = [ - { name: "user 1", username: "u1", passwordHash, avatarURL: null }, - { name: "user 2", username: "u2", passwordHash, avatarURL: null }, + { name: "user 1", username: "u1", email: "user1@example.com", passwordHash, avatarURL: null }, + { name: "user 2", username: "u2", email: "user2@example.com", passwordHash, avatarURL: null }, // anything past here is just to have more users to assign issues to - { name: "user 3", username: "u3", passwordHash, avatarURL: null }, - { name: "user 4", username: "u4", passwordHash, avatarURL: null }, - { name: "user 5", username: "u5", passwordHash, avatarURL: null }, - { name: "user 6", username: "u6", passwordHash, avatarURL: null }, - { name: "user 7", username: "u7", passwordHash, avatarURL: null }, - { name: "user 8", username: "u8", passwordHash, avatarURL: null }, + { name: "user 3", username: "u3", email: "user3@example.com", passwordHash, avatarURL: null }, + { name: "user 4", username: "u4", email: "user4@example.com", passwordHash, avatarURL: null }, + { name: "user 5", username: "u5", email: "user5@example.com", passwordHash, avatarURL: null }, + { name: "user 6", username: "u6", email: "user6@example.com", passwordHash, avatarURL: null }, + { name: "user 7", username: "u7", email: "user7@example.com", passwordHash, avatarURL: null }, + { name: "user 8", username: "u8", email: "user8@example.com", passwordHash, avatarURL: null }, ]; async function seed() { diff --git a/packages/backend/src/db/queries/users.ts b/packages/backend/src/db/queries/users.ts index 83f1fc6..ca9a567 100644 --- a/packages/backend/src/db/queries/users.ts +++ b/packages/backend/src/db/queries/users.ts @@ -5,10 +5,14 @@ import { db } from "../client"; export async function createUser( name: string, username: string, + email: string, passwordHash: string, avatarURL?: string | null, ) { - const [user] = await db.insert(User).values({ name, username, passwordHash, avatarURL }).returning(); + const [user] = await db + .insert(User) + .values({ name, username, email, passwordHash, avatarURL }) + .returning(); return user; } @@ -22,6 +26,11 @@ export async function getUserByUsername(username: string) { return user; } +export async function getUserByEmail(email: string) { + const [user] = await db.select().from(User).where(eq(User.email, email)); + return user; +} + export async function updateById( id: number, updates: { diff --git a/packages/backend/src/routes/auth/register.ts b/packages/backend/src/routes/auth/register.ts index 6ca2ab0..37d8b3b 100644 --- a/packages/backend/src/routes/auth/register.ts +++ b/packages/backend/src/routes/auth/register.ts @@ -2,6 +2,7 @@ import { RegisterRequestSchema } from "@sprint/shared"; import type { BunRequest } from "bun"; import { buildAuthCookie, generateToken, hashPassword } from "../../auth/utils"; import { createSession, createUser, getUserByUsername } from "../../db/queries"; +import { getUserByEmail } from "../../db/queries/users"; import { errorResponse, parseJsonBody } from "../../validation"; export default async function register(req: BunRequest) { @@ -12,15 +13,20 @@ export default async function register(req: BunRequest) { const parsed = await parseJsonBody(req, RegisterRequestSchema); if ("error" in parsed) return parsed.error; - const { name, username, password, avatarURL } = parsed.data; + const { name, username, email, password, avatarURL } = parsed.data; - const existing = await getUserByUsername(username); - if (existing) { + const existingUsername = await getUserByUsername(username); + if (existingUsername) { return errorResponse("username already taken", "USERNAME_TAKEN", 400); } + const existingEmail = await getUserByEmail(email); + if (existingEmail) { + return errorResponse("email already registered", "EMAIL_TAKEN", 400); + } + const passwordHash = await hashPassword(password); - const user = await createUser(name, username, passwordHash, avatarURL); + const user = await createUser(name, username, email, passwordHash, avatarURL); if (!user) { return errorResponse("failed to create user", "USER_CREATE_ERROR", 500); } diff --git a/packages/backend/src/routes/subscription/create-checkout-session.ts b/packages/backend/src/routes/subscription/create-checkout-session.ts index b0e72d0..61f1aa5 100644 --- a/packages/backend/src/routes/subscription/create-checkout-session.ts +++ b/packages/backend/src/routes/subscription/create-checkout-session.ts @@ -39,10 +39,8 @@ async function handler(req: AuthedRequest) { const quantity = Math.max(1, totalMembers - 5); const priceId = billingPeriod === "annual" ? STRIPE_PRICE_ANNUAL : STRIPE_PRICE_MONTHLY; - // build customer data - use username as email if no email field exists - const customerEmail = user.username.includes("@") - ? user.username - : `${user.username}@localhost.local`; + // use the user's email from the database + const customerEmail = user.email; const session = await stripe.checkout.sessions.create({ customer_email: customerEmail, diff --git a/packages/frontend/src/components/login-form.tsx b/packages/frontend/src/components/login-form.tsx index 9093341..68187a8 100644 --- a/packages/frontend/src/components/login-form.tsx +++ b/packages/frontend/src/components/login-form.tsx @@ -1,6 +1,6 @@ /** biome-ignore-all lint/correctness/useExhaustiveDependencies: <> */ -import { USER_NAME_MAX_LENGTH, USER_USERNAME_MAX_LENGTH } from "@sprint/shared"; +import { USER_EMAIL_MAX_LENGTH, USER_NAME_MAX_LENGTH, USER_USERNAME_MAX_LENGTH } from "@sprint/shared"; import { useEffect, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import Avatar from "@/components/avatar"; @@ -36,6 +36,7 @@ export default function LogInForm({ const [name, setName] = useState(""); const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [avatarURL, setAvatarUrl] = useState(null); const [error, setError] = useState(""); @@ -75,7 +76,7 @@ export default function LogInForm({ }; const register = () => { - if (name.trim() === "" || username.trim() === "" || password.trim() === "") { + if (name.trim() === "" || username.trim() === "" || email.trim() === "" || password.trim() === "") { return; } @@ -85,6 +86,7 @@ export default function LogInForm({ body: JSON.stringify({ name, username, + email, password, avatarURL, }), @@ -129,6 +131,7 @@ export default function LogInForm({ setError(""); setSubmitAttempted(false); setAvatarUrl(null); + setEmail(""); requestAnimationFrame(() => focusFirstInput()); }; @@ -249,6 +252,15 @@ export default function LogInForm({ spellcheck={false} maxLength={USER_NAME_MAX_LENGTH} /> + setEmail(e.target.value)} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + submitAttempted={submitAttempted} + spellcheck={false} + maxLength={USER_EMAIL_MAX_LENGTH} + /> )} (),