diff --git a/lib/sanbase/accounts/user.ex b/lib/sanbase/accounts/user.ex index 15b1a2197e..7fc87b0e7e 100644 --- a/lib/sanbase/accounts/user.ex +++ b/lib/sanbase/accounts/user.ex @@ -83,6 +83,10 @@ defmodule Sanbase.Accounts.User do field(:is_superuser, :boolean, default: false) field(:twitter_id, :string) + field(:description, :string) + field(:website_link, :string) + field(:twitter_link, :string) + # GDPR related fields field(:privacy_policy_accepted, :boolean, default: false) field(:marketing_accepted, :boolean, default: false) @@ -168,7 +172,10 @@ defmodule Sanbase.Accounts.User do :twitter_id, :username, :name, - :registration_state + :registration_state, + :description, + :website_link, + :twitter_link ]) |> normalize_user_identificator(:username, attrs[:username]) |> normalize_user_identificator(:email, attrs[:email]) @@ -177,6 +184,8 @@ defmodule Sanbase.Accounts.User do |> validate_change(:username, &validate_username_change/2) |> validate_change(:email_candidate, &validate_email_candidate_change/2) |> validate_change(:avatar_url, &validate_url_change/2) + |> validate_change(:website_link, &validate_url_simple/2) + |> validate_change(:twitter_link, &validate_url_simple/2) |> unique_constraint(:email) |> unique_constraint(:username) |> unique_constraint(:stripe_customer_id) @@ -395,4 +404,12 @@ defmodule Sanbase.Accounts.User do ) |> Repo.all() end + + def update_profile(%__MODULE__{} = user, attrs) do + user + |> changeset(attrs) + |> Repo.update() + + # |> emit_event(:update_profile, %{}) + end end diff --git a/lib/sanbase/accounts/user/user_validation.ex b/lib/sanbase/accounts/user/user_validation.ex index f3e2fa233d..8faad4a325 100644 --- a/lib/sanbase/accounts/user/user_validation.ex +++ b/lib/sanbase/accounts/user/user_validation.ex @@ -40,10 +40,17 @@ defmodule Sanbase.Accounts.User.Validation do end end - def validate_url_change(:avatar_url, url) do + def validate_url_change(field, url) do case Sanbase.Validation.valid_url?(url) do :ok -> [] - {:error, msg} -> [avatar_url: msg] + {:error, msg} -> [{field, msg}] + end + end + + def validate_url_simple(field, url) do + case Sanbase.Validation.valid_url_simple?(url) do + true -> [] + false -> [{field, "Invalid URL"}] end end end diff --git a/lib/sanbase/utils/validation.ex b/lib/sanbase/utils/validation.ex index 04b229700e..d0ee069544 100644 --- a/lib/sanbase/utils/validation.ex +++ b/lib/sanbase/utils/validation.ex @@ -125,6 +125,11 @@ defmodule Sanbase.Validation do end end + def valid_url_simple?(url) do + uri = URI.parse(url) + uri.scheme != nil and uri.host != nil + end + # Private functions defp time_window_format_check(time_window) do diff --git a/lib/sanbase_web/graphql/resolvers/user/user_resolver.ex b/lib/sanbase_web/graphql/resolvers/user/user_resolver.ex index 6a001de47b..cf47dfd266 100644 --- a/lib/sanbase_web/graphql/resolvers/user/user_resolver.ex +++ b/lib/sanbase_web/graphql/resolvers/user/user_resolver.ex @@ -319,4 +319,17 @@ defmodule SanbaseWeb.Graphql.Resolvers.UserResolver do {:ok, Dataloader.get(loader, SanbaseDataloader, :users_by_id, user_id)} end) end + + def update_profile(_root, args, %{context: %{auth: %{current_user: user}}}) do + case User.update_profile(user, args) do + {:ok, user} -> + {:ok, user} + + {:error, changeset} -> + { + :error, + message: "Cannot update user profile", details: changeset_errors(changeset) + } + end + end end diff --git a/lib/sanbase_web/graphql/schema/queries/user_queries.ex b/lib/sanbase_web/graphql/schema/queries/user_queries.ex index 32bbddc94b..e3fab51f08 100644 --- a/lib/sanbase_web/graphql/schema/queries/user_queries.ex +++ b/lib/sanbase_web/graphql/schema/queries/user_queries.ex @@ -208,5 +208,14 @@ defmodule SanbaseWeb.Graphql.Schema.UserQueries do middleware(UserAuth) resolve(&UserResolver.self_reset_api_rate_limits/3) end + + field :update_user_profile, :user do + arg(:description, :string) + arg(:website_link, :string) + arg(:twitter_link, :string) + + middleware(JWTAuth) + resolve(&UserResolver.update_profile/3) + end end end diff --git a/lib/sanbase_web/graphql/schema/types/user_types.ex b/lib/sanbase_web/graphql/schema/types/user_types.ex index 0ea9dd560a..41ffa3f2a8 100644 --- a/lib/sanbase_web/graphql/schema/types/user_types.ex +++ b/lib/sanbase_web/graphql/schema/types/user_types.ex @@ -50,6 +50,9 @@ defmodule SanbaseWeb.Graphql.UserTypes do field(:name, :string) field(:username, :string) field(:avatar_url, :string) + field(:description, :string) + field(:website_link, :string) + field(:twitter_link, :string) field :triggers, list_of(:trigger) do cache_resolve(&UserTriggerResolver.public_triggers/3, ttl: 60) @@ -129,6 +132,9 @@ defmodule SanbaseWeb.Graphql.UserTypes do field(:stripe_customer_id, :string) field(:inserted_at, non_null(:datetime)) field(:updated_at, non_null(:datetime)) + field(:description, :string) + field(:website_link, :string) + field(:twitter_link, :string) field :queries_executions_info, :queries_executions_info do resolve(&UserResolver.queries_executions_info/3) diff --git a/priv/repo/migrations/20241126110304_add_user_profile_fields.exs b/priv/repo/migrations/20241126110304_add_user_profile_fields.exs new file mode 100644 index 0000000000..5c6c0ea2a9 --- /dev/null +++ b/priv/repo/migrations/20241126110304_add_user_profile_fields.exs @@ -0,0 +1,19 @@ +defmodule Sanbase.Repo.Migrations.AddUserProfileFields do + use Ecto.Migration + + def change do + alter table(:users) do + # Text field for user's description/bio + add(:description, :text) + + # Boolean flag for Santiment team membership + add(:is_santiment_team, :boolean, default: false) + + # Social links + add(:twitter_link, :string) + add(:website_link, :string) + end + + create(index(:users, [:is_santiment_team])) + end +end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index cc6284956f..3785978d46 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -2,8 +2,8 @@ -- PostgreSQL database dump -- --- Dumped from database version 14.12 (Homebrew) --- Dumped by pg_dump version 14.12 (Homebrew) +-- Dumped from database version 15.1 (Homebrew) +-- Dumped by pg_dump version 15.1 (Homebrew) SET statement_timeout = 0; SET lock_timeout = 0; @@ -4559,7 +4559,11 @@ CREATE TABLE public.users ( is_superuser boolean DEFAULT false, twitter_id character varying(255) DEFAULT NULL::character varying, name character varying(255), - registration_state jsonb DEFAULT '{"state": "init"}'::jsonb + registration_state jsonb DEFAULT '{"state": "init"}'::jsonb, + description text, + is_santiment_team boolean DEFAULT false, + twitter_link character varying(255), + website_link character varying(255) ); @@ -7806,6 +7810,13 @@ CREATE UNIQUE INDEX users_email_index ON public.users USING btree (email); CREATE UNIQUE INDEX users_email_token_index ON public.users USING btree (email_token); +-- +-- Name: users_is_santiment_team_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX users_is_santiment_team_index ON public.users USING btree (is_santiment_team); + + -- -- Name: users_stripe_customer_id_index; Type: INDEX; Schema: public; Owner: - -- @@ -9674,9 +9685,11 @@ INSERT INTO public."schema_migrations" (version) VALUES (20241029080754); INSERT INTO public."schema_migrations" (version) VALUES (20241029082533); INSERT INTO public."schema_migrations" (version) VALUES (20241029151959); INSERT INTO public."schema_migrations" (version) VALUES (20241030141825); +INSERT INTO public."schema_migrations" (version) VALUES (20241104061632); INSERT INTO public."schema_migrations" (version) VALUES (20241104115340); INSERT INTO public."schema_migrations" (version) VALUES (20241108112754); INSERT INTO public."schema_migrations" (version) VALUES (20241112094924); INSERT INTO public."schema_migrations" (version) VALUES (20241114140339); INSERT INTO public."schema_migrations" (version) VALUES (20241114141110); INSERT INTO public."schema_migrations" (version) VALUES (20241116104556); +INSERT INTO public."schema_migrations" (version) VALUES (20241126110304); diff --git a/test/sanbase_web/graphql/user/user_api_test.exs b/test/sanbase_web/graphql/user/user_api_test.exs index 4aa179e590..cd3640ac03 100644 --- a/test/sanbase_web/graphql/user/user_api_test.exs +++ b/test/sanbase_web/graphql/user/user_api_test.exs @@ -384,4 +384,65 @@ defmodule SanbaseWeb.Graphql.UserApiTest do |> json_response(200) |> get_in(["data", "currentUser", "isModerator"]) end + + describe "Update user profile" do + test "successfully updates profile fields", %{conn: conn} do + mutation = """ + mutation { + updateUserProfile( + description: "Test description" + websiteLink: "https://example.com" + twitterLink: "https://twitter.com/test" + ) { + description + websiteLink + twitterLink + } + } + """ + + result = execute_mutation(conn, mutation, "updateUserProfile") + + assert result["description"] == "Test description" + assert result["websiteLink"] == "https://example.com" + assert result["twitterLink"] == "https://twitter.com/test" + end + + test "can update individual fields", %{conn: conn} do + mutation = """ + mutation { + updateUserProfile(description: "Only description updated") { + description + websiteLink + twitterLink + } + } + """ + + result = execute_mutation(conn, mutation, "updateUserProfile") + + assert result["description"] == "Only description updated" + assert result["websiteLink"] == nil + assert result["twitterLink"] == nil + end + + test "invalid URL format returns error", %{conn: conn} do + mutation = """ + mutation { + updateUserProfile(websiteLink: "invalid-url") { + websiteLink + } + } + """ + + result = + conn + |> post("/graphql", mutation_skeleton(mutation)) + + error = json_response(result, 200)["errors"] |> hd() + + assert error["details"] == %{"website_link" => ["Invalid URL"]} + assert error["message"] == "Cannot update user profile" + end + end end