diff --git a/backend/drizzle/0005_heavy_la_nuit.sql b/backend/drizzle/0005_heavy_la_nuit.sql new file mode 100644 index 0000000..85bb58c --- /dev/null +++ b/backend/drizzle/0005_heavy_la_nuit.sql @@ -0,0 +1,12 @@ +CREATE TABLE "keywords" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "keyword" text NOT NULL, + "source" text DEFAULT 'user' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "phone" varchar(20);--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "cpf" varchar(14);--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "technologies" text[] DEFAULT '{}';--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "level" varchar(50);--> statement-breakpoint +CREATE UNIQUE INDEX "keywords_keyword_unique" ON "keywords" USING btree ("keyword"); \ No newline at end of file diff --git a/backend/drizzle/0006_happy_gateway.sql b/backend/drizzle/0006_happy_gateway.sql new file mode 100644 index 0000000..89c1853 --- /dev/null +++ b/backend/drizzle/0006_happy_gateway.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "role" varchar(20) DEFAULT 'user' NOT NULL; \ No newline at end of file diff --git a/backend/drizzle/meta/0005_snapshot.json b/backend/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..09d6839 --- /dev/null +++ b/backend/drizzle/meta/0005_snapshot.json @@ -0,0 +1,611 @@ +{ + "id": "d46d415d-9787-44ad-962a-a2405d5896e8", + "prevId": "5f2483f7-3b14-415f-93bc-c9a77d90b2b1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "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": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_unique": { + "name": "accounts_provider_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "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 + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "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": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credentials_user_id_unique": { + "name": "credentials_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "credentials_email_unique": { + "name": "credentials_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keywords": { + "name": "keywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "keywords_keyword_unique": { + "name": "keywords_keyword_unique", + "columns": [ + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_jobs": { + "name": "saved_jobs", + "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 + }, + "job_link": { + "name": "job_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'saved'" + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "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": { + "saved_jobs_user_id_users_id_fk": { + "name": "saved_jobs_user_id_users_id_fk", + "tableFrom": "saved_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "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 + }, + "keywords": { + "name": "keywords", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "search_location": { + "name": "search_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "search_language": { + "name": "search_language", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "remote_only": { + "name": "remote_only", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "job_types": { + "name": "job_types", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "email_notifications": { + "name": "email_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": 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": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_unique": { + "name": "user_preferences_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(14)", + "primaryKey": false, + "notNull": false + }, + "technologies": { + "name": "technologies", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "level": { + "name": "level", + "type": "varchar(50)", + "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_username_unique": { + "name": "users_username_unique", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "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/backend/drizzle/meta/0006_snapshot.json b/backend/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..397c618 --- /dev/null +++ b/backend/drizzle/meta/0006_snapshot.json @@ -0,0 +1,618 @@ +{ + "id": "facfeeea-cf12-4191-bc05-188150aaceb9", + "prevId": "d46d415d-9787-44ad-962a-a2405d5896e8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "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": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_unique": { + "name": "accounts_provider_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "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 + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "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": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credentials_user_id_unique": { + "name": "credentials_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "credentials_email_unique": { + "name": "credentials_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keywords": { + "name": "keywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "keywords_keyword_unique": { + "name": "keywords_keyword_unique", + "columns": [ + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_jobs": { + "name": "saved_jobs", + "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 + }, + "job_link": { + "name": "job_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'saved'" + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "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": { + "saved_jobs_user_id_users_id_fk": { + "name": "saved_jobs_user_id_users_id_fk", + "tableFrom": "saved_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "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 + }, + "keywords": { + "name": "keywords", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "search_location": { + "name": "search_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "search_language": { + "name": "search_language", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "remote_only": { + "name": "remote_only", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "job_types": { + "name": "job_types", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "email_notifications": { + "name": "email_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": 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": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_unique": { + "name": "user_preferences_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(14)", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "technologies": { + "name": "technologies", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "level": { + "name": "level", + "type": "varchar(50)", + "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_username_unique": { + "name": "users_username_unique", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "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/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index 10b0317..cfae5aa 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -36,6 +36,20 @@ "when": 1780927293836, "tag": "0004_small_psylocke", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1781886747704, + "tag": "0005_heavy_la_nuit", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1781892324992, + "tag": "0006_happy_gateway", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/src/db/schema/users.ts b/backend/src/db/schema/users.ts index 6a73c44..bc5b617 100644 --- a/backend/src/db/schema/users.ts +++ b/backend/src/db/schema/users.ts @@ -5,6 +5,7 @@ import { timestamp, uniqueIndex, uuid, + varchar, } from "drizzle-orm/pg-core"; import { InferInsertModel, InferSelectModel } from "drizzle-orm"; @@ -24,6 +25,11 @@ export const users = pgTable( emailVerified: boolean("email_verified").default(false).notNull(), avatarUrl: text("avatar_url"), + phone: varchar("phone", { length: 20 }), + cpf: varchar("cpf", { length: 14 }), + role: varchar("role", { length: 20 }).default("user").notNull(), + technologies: text("technologies").array().default([]), + level: varchar("level", { length: 50 }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), diff --git a/backend/src/modules/auth/credentials.service.ts b/backend/src/modules/auth/credentials.service.ts index ca69b73..3cd8c38 100644 --- a/backend/src/modules/auth/credentials.service.ts +++ b/backend/src/modules/auth/credentials.service.ts @@ -32,7 +32,7 @@ export class CredentialsService { async register( input: RegisterInput, ): Promise<{ user: User; session: Session }> { - const { email, password, name } = RegisterSchema.parse(input); + const { email, password, name, phone, cpf, technologies, level, role } = RegisterSchema.parse(input); const existingCredential = await db.query.credentials.findFirst({ where: eq(credentials.email, email), @@ -49,7 +49,17 @@ export class CredentialsService { const [user] = await db .insert(users) - .values({ email, displayName: name, username, emailVerified: false }) + .values({ + email, + displayName: name, + username, + emailVerified: false, + phone, + cpf, + technologies, + level, + role + }) .returning(); await db.insert(credentials).values({ userId: user.id, email, passwordHash }); diff --git a/backend/src/modules/types/credentials.types.ts b/backend/src/modules/types/credentials.types.ts index 92717e5..55cecf4 100644 --- a/backend/src/modules/types/credentials.types.ts +++ b/backend/src/modules/types/credentials.types.ts @@ -4,6 +4,11 @@ export const RegisterSchema = z.object({ email: z.string().email("Email inválido"), password: z.string().min(8, "Senha deve ter no mínimo 8 caracteres"), name: z.string().min(1, "Nome é obrigatório").optional(), + phone: z.string().optional(), + cpf: z.string().optional(), + technologies: z.array(z.string()).optional(), + level: z.string().optional(), + role: z.enum(["user", "admin"]).optional().default("user"), }); export const LoginSchema = z.object({ diff --git a/frontend/src/components/login/RegisterSide.tsx b/frontend/src/components/login/RegisterSide.tsx index b354149..37fcd87 100644 --- a/frontend/src/components/login/RegisterSide.tsx +++ b/frontend/src/components/login/RegisterSide.tsx @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { register, getGoogleAuthUrl, getGithubAuthUrl, getLinkedinAuthUrl } from "@/services/authService"; +import { Badge } from "@/components/ui/badge"; import { Image } from "@unpic/react"; import { motion } from "framer-motion"; import { ArrowLeft, Eye, EyeOff } from "lucide-react"; @@ -19,6 +20,13 @@ const STATIC_STARS = Array.from({ length: 40 }).map((_, i) => { }; }); +const POPULAR_TECHNOLOGIES = [ + "React", "Node.js", "Python", "Java", "TypeScript", + "Vue.js", "Angular", "Go", "Ruby", "C#", + "PHP", "Docker", "AWS", "SQL", "MongoDB", + "Flutter", "React Native" +]; + function StarsBackground() { return (
@@ -49,18 +57,32 @@ function StarsBackground() { } export default function RegisterSide() { + const [role, setRole] = useState<"user" | "admin">("user"); const [showPassword, setShowPassword] = useState(false); const [nome, setNome] = useState(""); const [email, setEmail] = useState(""); const [telefone, setTelefone] = useState(""); const [password, setPassword] = useState(""); const [cpf, setCpf] = useState(""); + const [level, setLevel] = useState(""); + const [selectedTechnologies, setSelectedTechnologies] = useState([]); + + const toggleTechnology = (tech: string) => { + if (isLoading) return; + setSelectedTechnologies(prev => + prev.includes(tech) + ? prev.filter(t => t !== tech) + : [...prev, tech] + ); + }; const [nomeError, setNomeError] = useState(""); const [emailError, setEmailError] = useState(""); const [telefoneError, setTelefoneError] = useState(""); const [passwordError, setPasswordError] = useState(""); const [cpfError, setCpfError] = useState(""); + const [levelError, setLevelError] = useState(""); + const [technologiesError, setTechnologiesError] = useState(""); const [isLoading, setIsLoading] = useState(false); const [apiError, setApiError] = useState(""); @@ -116,6 +138,8 @@ export default function RegisterSide() { setTelefoneError(""); setPasswordError(""); setCpfError(""); + setLevelError(""); + setTechnologiesError(""); setApiError(""); let isValid = true; @@ -154,6 +178,18 @@ export default function RegisterSide() { isValid = false; } + if (role === "admin") { + if (!level) { + setLevelError("A seleção de nível é obrigatória."); + isValid = false; + } + + if (selectedTechnologies.length === 0) { + setTechnologiesError("Selecione ao menos uma tecnologia."); + isValid = false; + } + } + if (isValid) { setIsLoading(true); try { @@ -161,6 +197,11 @@ export default function RegisterSide() { email: email, password: password, name: nome, + phone: telefone, + cpf: cpf, + level: role === "admin" ? level : undefined, + technologies: role === "admin" ? selectedTechnologies : undefined, + role: role, }); window.location.href = "/login?registered=true"; } catch (error: any) { @@ -200,6 +241,22 @@ export default function RegisterSide() {
+
+ + +
{apiError && (

{apiError}

@@ -237,6 +294,41 @@ export default function RegisterSide() { setCpf(formatCpf(e.target.value))} placeholder="091.000.000-00" disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${cpfError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} /> {cpfError &&

{cpfError}

}
+ {role === "admin" && ( + <> +
+ + + {levelError &&

{levelError}

} +
+
+ +
+ {POPULAR_TECHNOLOGIES.map((tech) => { + const isSelected = selectedTechnologies.includes(tech); + return ( + toggleTechnology(tech)} + > + {tech} + + ); + })} +
+ {technologiesError &&

{technologiesError}

} +
+ + )} {isLoading ? "Cadastrando..." : "Cadastrar"} diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts index 2b1e2f8..6b2084a 100644 --- a/frontend/src/services/authService.ts +++ b/frontend/src/services/authService.ts @@ -53,6 +53,9 @@ export interface RegisterData { name: string; phone?: string; cpf?: string; + technologies?: string[]; + level?: string; + role?: "user" | "admin"; } export async function login(credentials: LoginCredentials) { @@ -82,6 +85,9 @@ export async function register(userData: RegisterData) { name: userData.name, phone: userData.phone, cpf: userData.cpf, + technologies: userData.technologies, + level: userData.level, + role: userData.role, }), credentials: "include", }); diff --git a/frontend/tests/unit/components/login/RegisterSide.test.tsx b/frontend/tests/unit/components/login/RegisterSide.test.tsx index 735ed2c..ba5435e 100644 --- a/frontend/tests/unit/components/login/RegisterSide.test.tsx +++ b/frontend/tests/unit/components/login/RegisterSide.test.tsx @@ -72,20 +72,26 @@ describe("RegisterSide", () => { it("mostra erros obrigatórios ao submeter vazio", async () => { render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); expect(await screen.findByText(/campo de nome é obrigatório/i)).toBeInTheDocument(); expect(await screen.findByText(/campo de e-mail é obrigatório/i)).toBeInTheDocument(); expect(await screen.findByText(/campo de telefone é obrigatório/i)).toBeInTheDocument(); expect(await screen.findByText(/campo de senha é obrigatório/i)).toBeInTheDocument(); + expect(await screen.findByText(/seleção de nível é obrigatória/i)).toBeInTheDocument(); + expect(await screen.findByText(/selecione ao menos uma tecnologia/i)).toBeInTheDocument(); }); it("valida CPF inválido quando preenchido", async () => { render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Usuário" } }); fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); fireEvent.change(screen.getByLabelText(/cpf/i), { target: { value: "123" } }); + fireEvent.change(screen.getByLabelText(/nível/i), { target: { value: "Júnior" } }); + fireEvent.click(screen.getByText("React")); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); expect(await screen.findByText(/cpf inválido/i)).toBeInTheDocument(); }); @@ -93,28 +99,61 @@ describe("RegisterSide", () => { it("envia formulário válido sem CPF", async () => { mockRegister.mockResolvedValueOnce({ message: "Usuário criado" }); render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); + fireEvent.change(screen.getByLabelText(/nível/i), { target: { value: "Júnior" } }); + fireEvent.click(screen.getByText("React")); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); await waitFor(() => { expect(mockRegister).toHaveBeenCalledWith({ email: "bene@teste.com", password: "123456", name: "Bene", + phone: "+5534999999999", + cpf: "", + level: "Júnior", + technologies: ["React"], + role: "admin", }); }); expect(window.location.href).toBe("/login?registered=true"); }); + it("envia formulário válido como usuário (sem tecnologias/nível)", async () => { + mockRegister.mockResolvedValueOnce({ message: "Usuário criado" }); + render(); + fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); + fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); + fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); + await waitFor(() => { + expect(mockRegister).toHaveBeenCalledWith({ + email: "bene@teste.com", + password: "123456", + name: "Bene", + phone: "+5534999999999", + cpf: "", + level: undefined, + technologies: undefined, + role: "user" + }); + }); + }); + it("exibe erro da API", async () => { mockRegister.mockRejectedValueOnce(new Error("Email já cadastrado")); render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); + fireEvent.change(screen.getByLabelText(/nível/i), { target: { value: "Júnior" } }); + fireEvent.click(screen.getByText("React")); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); expect(await screen.findByText(/Email já cadastrado/i)).toBeInTheDocument(); }); @@ -122,10 +161,13 @@ describe("RegisterSide", () => { it("mostra loading durante requisição", async () => { mockRegister.mockImplementation(() => new Promise(() => {})); render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); + fireEvent.change(screen.getByLabelText(/nível/i), { target: { value: "Júnior" } }); + fireEvent.click(screen.getByText("React")); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); expect(await screen.findByRole("button", { name: /cadastrando\.\.\./i })).toBeDisabled(); }); @@ -142,6 +184,7 @@ describe("RegisterSide", () => { it("desabilita inputs durante loading", async () => { mockRegister.mockImplementation(() => new Promise(() => {})); render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); const nomeInput = screen.getByLabelText(/nome/i); const emailInput = screen.getByLabelText(/email/i); const telefoneInput = screen.getByPlaceholderText(/\(34\)/i); @@ -150,12 +193,15 @@ describe("RegisterSide", () => { fireEvent.change(emailInput, { target: { value: "bene@teste.com" } }); fireEvent.change(telefoneInput, { target: { value: "+5534999999999" } }); fireEvent.change(passwordInput, { target: { value: "123456" } }); + fireEvent.change(screen.getByLabelText(/nível/i), { target: { value: "Júnior" } }); + fireEvent.click(screen.getByText("React")); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); await waitFor(() => { expect(nomeInput).toBeDisabled(); expect(emailInput).toBeDisabled(); expect(telefoneInput).toBeDisabled(); expect(passwordInput).toBeDisabled(); + expect(screen.getByLabelText(/nível/i)).toBeDisabled(); }); });