From 5ee848b2b2d911db7ad3079e4bc6c1e31185e92e Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Fri, 15 Aug 2025 19:02:44 +0100 Subject: [PATCH 1/6] feat: add endpoints for internationalization --- lib/atlas/accounts/user_preferences.ex | 5 +- .../controllers/preferences_controller.ex | 140 +++++ lib/atlas_web/router.ex | 3 + priv/static/swagger.json | 486 ++++++++++++------ 4 files changed, 470 insertions(+), 164 deletions(-) create mode 100644 lib/atlas_web/controllers/preferences_controller.ex diff --git a/lib/atlas/accounts/user_preferences.ex b/lib/atlas/accounts/user_preferences.ex index 2d9616e..ada33a3 100644 --- a/lib/atlas/accounts/user_preferences.ex +++ b/lib/atlas/accounts/user_preferences.ex @@ -6,15 +6,12 @@ defmodule Atlas.Accounts.UserPreference do use Ecto.Schema import Ecto.Changeset - @primary_key {:id, :binary_id, autogenerate: true} - @foreign_key_type :binary_id - @languages ~w(pt-PT en-US) schema "user_preferences" do field :language, :string - belongs_to :user, Atlas.Accounts.User + belongs_to :user, Atlas.Accounts.User, type: :binary_id timestamps() end diff --git a/lib/atlas_web/controllers/preferences_controller.ex b/lib/atlas_web/controllers/preferences_controller.ex new file mode 100644 index 0000000..b39346a --- /dev/null +++ b/lib/atlas_web/controllers/preferences_controller.ex @@ -0,0 +1,140 @@ +defmodule AtlasWeb.PreferencesController do + use AtlasWeb, :controller + use PhoenixSwagger + + alias Atlas.Accounts + + def get_language(conn, _params) do + {user, _session} = Guardian.Plug.current_resource(conn) + language = Accounts.get_user_language(user.id) + json(conn, %{language: language}) + end + + swagger_path :get_language do + get("/v1/auth/language") + summary("Get user language preference") + description("Returns the current language preference for the authenticated user.") + produces("application/json") + tag("Preferences") + operation_id("get_language") + response(200, "Language preference returned successfully", Schema.ref(:LanguageResponse)) + security([%{Bearer: []}]) + end + + def update_language(conn, %{"language" => language}) do + {user, _session} = Guardian.Plug.current_resource(conn) + Accounts.set_user_language(user.id, language) + json(conn, %{language: language}) + end + + def update_language(conn, _params) do + conn + |> put_status(:bad_request) + |> json(%{error: "Language parameter is required"}) + end + + swagger_path :update_language do + post("/v1/auth/language") + summary("Update user language preference") + description("Updates the language preference for the authenticated user.") + produces("application/json") + consumes("application/json") + tag("Preferences") + operation_id("update_language") + + parameters do + language_request(:body, Schema.ref(:LanguageRequest), "Language preference to set", + required: true + ) + end + + response(200, "Language preference updated successfully", Schema.ref(:LanguageResponse)) + response(400, "Bad request - Missing language parameter", Schema.ref(:ErrorResponse)) + security([%{Bearer: []}]) + end + + def available_languages(conn, _params) do + json(conn, %{languages: ["pt-PT", "en-US"]}) + end + + swagger_path :available_languages do + post("/v1/auth/available_languages") + summary("Get available languages") + description("Returns a list of all supported languages.") + produces("application/json") + tag("Preferences") + operation_id("available_languages") + + response( + 200, + "Available languages returned successfully", + Schema.ref(:AvailableLanguagesResponse) + ) + + security([%{Bearer: []}]) + end + + def swagger_definitions do + %{ + LanguageRequest: + swagger_schema do + title("LanguageRequest") + description("Request schema for updating language preference") + + properties do + language(:string, "Language", required: true, enum: ["pt-PT", "en-US"]) + end + + example(%{ + language: "en-US" + }) + end, + LanguageResponse: + swagger_schema do + title("LanguageResponse") + description("Response schema for language preference") + + properties do + language(:string, "Current language preference", + required: true, + enum: ["pt-PT", "en-US"] + ) + end + + example(%{ + language: "en-US" + }) + end, + AvailableLanguagesResponse: + swagger_schema do + title("AvailableLanguagesResponse") + description("Response schema for available languages") + + properties do + languages(:array, "List of supported language codes", + required: true, + items: %{type: :string} + ) + end + + example(%{ + languages: ["pt-PT", "en-US"] + }) + end, + ErrorResponse: + swagger_schema do + title("ErrorResponse") + description("Error response schema") + type(:object) + + properties do + error(:string, "Error message", required: true) + end + + example(%{ + error: "Language parameter is required" + }) + end + } + end +end diff --git a/lib/atlas_web/router.ex b/lib/atlas_web/router.ex index 2faad83..afef922 100644 --- a/lib/atlas_web/router.ex +++ b/lib/atlas_web/router.ex @@ -44,6 +44,9 @@ defmodule AtlasWeb.Router do pipe_through :auth scope "/auth" do + get "/language", PreferencesController, :get_language + post "/language", PreferencesController, :update_language + post "/available_languages", PreferencesController, :available_languages post "/sign_out", AuthController, :sign_out get "/me", AuthController, :me get "/sessions", AuthController, :sessions diff --git a/priv/static/swagger.json b/priv/static/swagger.json index 292adfd..4de37f5 100644 --- a/priv/static/swagger.json +++ b/priv/static/swagger.json @@ -5,6 +5,102 @@ }, "host": "localhost:4000", "definitions": { + "Job": { + "description": "A job in the system", + "properties": { + "attempted_at": { + "description": "Timestamp when the job was attempted", + "format": "date-time", + "type": "string" + }, + "completed_at": { + "description": "Timestamp when the job was completed", + "format": "date-time", + "type": "string" + }, + "id": { + "description": "ID of the job", + "type": "integer" + }, + "inserted_at": { + "description": "Timestamp when the job was created", + "format": "date-time", + "type": "string" + }, + "state": { + "description": "Status of the job", + "type": "string" + }, + "type": { + "description": "Type of the job", + "type": "string" + }, + "user_id": { + "description": "ID of the user who created the job", + "type": "string" + } + }, + "required": [ + "completed_at", + "attempted_at", + "inserted_at", + "user_id", + "state", + "type", + "id" + ], + "title": "Job", + "type": "object" + }, + "User": { + "description": "User schema", + "example": { + "user": { + "email": "a114437@alunos.uminho.pt", + "id": "d18472e7-5251-4027-884f-58b8a3a6abe5", + "inserted_at": "2025-07-15T18:10:27Z", + "name": "Leonardo Carvalho", + "updated_at": "2025-07-15T18:10:27Z" + } + }, + "properties": { + "email": { + "description": "User email", + "type": "string" + }, + "id": { + "description": "User ID", + "type": "integer" + }, + "inserted_at": { + "description": "Creation timestamp", + "format": "date-time", + "type": "string" + }, + "name": { + "description": "User name", + "type": "string" + }, + "type": { + "description": "User type", + "type": "string" + }, + "updated_at": { + "description": "Last update timestamp", + "format": "date-time", + "type": "string" + } + }, + "required": [ + "type", + "updated_at", + "email", + "inserted_at", + "id" + ], + "title": "User", + "type": "object" + }, "UserSession": { "description": "User session schema", "properties": { @@ -45,52 +141,6 @@ "title": "User Session", "type": "object" }, - "ErrorResponse": { - "description": "Error response schema", - "properties": { - "error": { - "description": "Error message", - "type": "string" - } - }, - "required": [ - "error" - ], - "title": "ErrorResponse", - "type": "object" - }, - "NoContentResponse": { - "description": "Response schema for no content", - "example": {}, - "properties": { - "message": { - "description": "Message indicating no content", - "type": "string" - } - }, - "required": [ - "message" - ], - "title": "NoContentResponse", - "type": "object" - }, - "ResetPasswordResponse": { - "description": "Response schema for successful password reset", - "example": { - "message": "Password reset successfully" - }, - "properties": { - "message": { - "description": "Message indicating successful password reset", - "type": "string" - } - }, - "required": [ - "message" - ], - "title": "ResetPasswordResponse", - "type": "object" - }, "SignInResponse": { "description": "Response schema for successful sign in", "example": { @@ -114,21 +164,35 @@ "title": "SignInResponse", "type": "object" }, - "SignOutResponse": { - "description": "Response schema for successful sign out", + "UnauthorizedResponse": { + "description": "Unauthorized response schema", + "properties": { + "error": { + "description": "Unauthorized error message", + "type": "string" + } + }, + "required": [ + "error" + ], + "title": "UnauthorizedResponse", + "type": "object" + }, + "ErrorResponse": { + "description": "Error response schema", "example": { - "message": "Signed out successfully" + "error": "Language parameter is required" }, "properties": { - "message": { - "description": "Message indicating successful sign out", + "error": { + "description": "Error message", "type": "string" } }, "required": [ - "message" + "error" ], - "title": "SignOutResponse", + "title": "ErrorResponse", "type": "object" }, "SuccessfulRefreshResponse": { @@ -148,67 +212,21 @@ "title": "SuccessfulRefreshResponse", "type": "object" }, - "UnauthorizedResponse": { - "description": "Unauthorized response schema", - "properties": { - "error": { - "description": "Unauthorized error message", - "type": "string" - } - }, - "required": [ - "error" - ], - "title": "UnauthorizedResponse", - "type": "object" - }, - "User": { - "description": "User schema", + "SignOutResponse": { + "description": "Response schema for successful sign out", "example": { - "user": { - "email": "a114437@alunos.uminho.pt", - "id": "d18472e7-5251-4027-884f-58b8a3a6abe5", - "inserted_at": "2025-07-15T18:10:27Z", - "name": "Leonardo Carvalho", - "updated_at": "2025-07-15T18:10:27Z" - } + "message": "Signed out successfully" }, "properties": { - "email": { - "description": "User email", - "type": "string" - }, - "id": { - "description": "User ID", - "type": "integer" - }, - "inserted_at": { - "description": "Creation timestamp", - "format": "date-time", - "type": "string" - }, - "name": { - "description": "User name", - "type": "string" - }, - "type": { - "description": "User type", - "type": "string" - }, - "updated_at": { - "description": "Last update timestamp", - "format": "date-time", + "message": { + "description": "Message indicating successful sign out", "type": "string" } }, "required": [ - "type", - "updated_at", - "email", - "inserted_at", - "id" + "message" ], - "title": "User", + "title": "SignOutResponse", "type": "object" }, "UserSessionsResponse": { @@ -240,62 +258,55 @@ "title": "UserSessionsResponse", "type": "object" }, - "Job": { - "description": "A job in the system", + "NoContentResponse": { + "description": "Response schema for no content", + "example": {}, "properties": { - "attempted_at": { - "description": "Timestamp when the job was attempted", - "format": "date-time", - "type": "string" - }, - "completed_at": { - "description": "Timestamp when the job was completed", - "format": "date-time", - "type": "string" - }, - "id": { - "description": "ID of the job", - "type": "integer" - }, - "inserted_at": { - "description": "Timestamp when the job was created", - "format": "date-time", - "type": "string" - }, - "state": { - "description": "Status of the job", - "type": "string" - }, - "type": { - "description": "Type of the job", + "message": { + "description": "Message indicating no content", "type": "string" - }, - "user_id": { - "description": "ID of the user who created the job", + } + }, + "required": [ + "message" + ], + "title": "NoContentResponse", + "type": "object" + }, + "ResetPasswordResponse": { + "description": "Response schema for successful password reset", + "example": { + "message": "Password reset successfully" + }, + "properties": { + "message": { + "description": "Message indicating successful password reset", "type": "string" } }, "required": [ - "completed_at", - "attempted_at", - "inserted_at", - "user_id", - "state", - "type", - "id" + "message" ], - "title": "Job", + "title": "ResetPasswordResponse", "type": "object" }, - "JobResponse": { - "description": "Response containing a single job", + "SuccessfulImport": { + "description": "Response for a successful import", "properties": { - "job": { - "$ref": "#/definitions/Job", - "description": "Details of the job" + "job_id": { + "description": "ID of the import job", + "type": "string" + }, + "message": { + "description": "Status message", + "type": "string" } }, - "title": "JobResponse", + "required": [ + "message", + "job_id" + ], + "title": "SuccessfulImport", "type": "object" }, "JobsResponse": { @@ -312,27 +323,111 @@ "title": "JobsResponse", "type": "object" }, - "SuccessfulImport": { - "description": "Response for a successful import", + "JobResponse": { + "description": "Response containing a single job", "properties": { - "job_id": { - "description": "ID of the import job", + "job": { + "$ref": "#/definitions/Job", + "description": "Details of the job" + } + }, + "title": "JobResponse", + "type": "object" + }, + "LanguageResponse": { + "description": "Response schema for language preference", + "example": { + "language": "en-US" + }, + "properties": { + "language": { + "description": "Current language preference", + "enum": [ + "pt-PT", + "en-US" + ], "type": "string" - }, - "message": { - "description": "Status message", + } + }, + "required": [ + "language" + ], + "title": "LanguageResponse", + "type": "object" + }, + "LanguageRequest": { + "description": "Request schema for updating language preference", + "example": { + "language": "en-US" + }, + "properties": { + "language": { + "description": "Language", + "enum": [ + "pt-PT", + "en-US" + ], "type": "string" } }, "required": [ - "message", - "job_id" + "language" ], - "title": "SuccessfulImport", + "title": "LanguageRequest", + "type": "object" + }, + "AvailableLanguagesResponse": { + "description": "Response schema for available languages", + "example": { + "languages": [ + "pt-PT", + "en-US" + ] + }, + "properties": { + "languages": { + "description": "List of supported language codes", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "languages" + ], + "title": "AvailableLanguagesResponse", "type": "object" } }, "paths": { + "/v1/auth/available_languages": { + "post": { + "description": "Returns a list of all supported languages.", + "operationId": "available_languages", + "parameters": [], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Available languages returned successfully", + "schema": { + "$ref": "#/definitions/AvailableLanguagesResponse" + } + } + }, + "security": [ + { + "Bearer": [] + } + ], + "summary": "Get available languages", + "tags": [ + "Preferences" + ] + } + }, "/v1/auth/forgot_password": { "post": { "description": "Sends password reset instructions to the user via email.", @@ -369,6 +464,77 @@ ] } }, + "/v1/auth/language": { + "get": { + "description": "Returns the current language preference for the authenticated user.", + "operationId": "get_language", + "parameters": [], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Language preference returned successfully", + "schema": { + "$ref": "#/definitions/LanguageResponse" + } + } + }, + "security": [ + { + "Bearer": [] + } + ], + "summary": "Get user language preference", + "tags": [ + "Preferences" + ] + }, + "post": { + "consumes": [ + "application/json" + ], + "description": "Updates the language preference for the authenticated user.", + "operationId": "update_language", + "parameters": [ + { + "description": "Language preference to set", + "in": "body", + "name": "language_request", + "required": true, + "schema": { + "$ref": "#/definitions/LanguageRequest" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Language preference updated successfully", + "schema": { + "$ref": "#/definitions/LanguageResponse" + } + }, + "400": { + "description": "Bad request - Missing language parameter", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "Bearer": [] + } + ], + "summary": "Update user language preference", + "tags": [ + "Preferences" + ] + } + }, "/v1/auth/me": { "get": { "description": "Returns the user in the current session.", From 61bbcd147a993423ccd83352b779fb671d86c0b8 Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Tue, 19 Aug 2025 22:26:06 +0100 Subject: [PATCH 2/6] chore: change languages functions to preferences functions --- lib/atlas/accounts.ex | 70 +++-- .../controllers/preferences_controller.ex | 142 ++------- lib/atlas_web/router.ex | 6 +- priv/static/swagger.json | 276 ++++-------------- test/atlas/preferences_test.exs | 44 +++ 5 files changed, 160 insertions(+), 378 deletions(-) create mode 100644 test/atlas/preferences_test.exs diff --git a/lib/atlas/accounts.ex b/lib/atlas/accounts.ex index af7d966..843dba8 100644 --- a/lib/atlas/accounts.ex +++ b/lib/atlas/accounts.ex @@ -99,10 +99,17 @@ defmodule Atlas.Accounts do {:error, %Ecto.Changeset{}} """ + def register_user(attrs) do - %User{} - |> User.registration_changeset(attrs) - |> Repo.insert() + Ecto.Multi.new() + |> Ecto.Multi.insert(:user, User.registration_changeset(%User{}, attrs)) + |> create_default_preferences_multi() + |> Repo.transaction() + |> case do + {:ok, %{user: user}} -> {:ok, user} + {:error, :user, changeset, _} -> {:error, changeset} + {:error, :preferences, changeset, _} -> {:error, changeset} + end end @doc """ @@ -539,64 +546,69 @@ defmodule Atlas.Accounts do Repo.delete(user_session) end - ## User Preference + ## User Preferences @doc """ - Gets the language preference for a given user. + Gets the set of preferences of a given user. ## Examples - iex> get_user_preference(1) + iex> get_user_preferences(1) %UserPreference{} - iex> get_user_preference(999) + iex> get_user_preferences(999) nil """ - def get_user_preference(user_id) do + def get_user_preferences(user_id) do Repo.get_by(UserPreference, user_id: user_id) end @doc """ - Gets the language string for a given user. - - Returns `nil` if no preference is set. + Gets a given preference from the user preferences. ## Examples - iex> get_user_language(1) - "pt-PT" + iex> get_user_preference(1, "language") + "en-US" - iex> get_user_language(999) + iex> get_user_preference(1, "void") nil """ - def get_user_language(user_id) do - Repo.one( - from up in UserPreference, - where: up.user_id == ^user_id, - select: up.language, - limit: 1 - ) + def get_user_preference(user_id, preference) do + preferences = get_user_preferences(user_id) + Map.get(preferences, String.to_atom(preference), nil) end @doc """ - Sets the language preference for a given user. - - If the preference exists, it updates only the language field. + Sets a given preference for a user. ## Examples - iex> set_user_language(1, "pt-PT") + iex> set_user_preference(1, "language", "en-US") {:ok, %UserPreference{}} - iex> set_user_language(1, "invalid") + iex> set_user_preference(1, "void", "none") {:error, %Ecto.Changeset{}} """ - def set_user_language(user_id, language) do + def set_user_preference(user_id, preference, value) do + preference = String.to_atom(preference) + attrs = %{preference => value, user_id: user_id} + %UserPreference{} - |> UserPreference.changeset(%{user_id: user_id, language: language}) + |> UserPreference.changeset(attrs) |> Repo.insert( - on_conflict: [set: [language: language]], + on_conflict: [set: [{preference, value}]], conflict_target: :user_id ) end + + defp create_default_preferences_multi(multi) do + Ecto.Multi.insert(multi, :preferences, fn %{user: user} -> + %UserPreference{} + |> UserPreference.changeset(%{ + user_id: user.id, + language: "en-US" + }) + end) + end end diff --git a/lib/atlas_web/controllers/preferences_controller.ex b/lib/atlas_web/controllers/preferences_controller.ex index b39346a..96e32d7 100644 --- a/lib/atlas_web/controllers/preferences_controller.ex +++ b/lib/atlas_web/controllers/preferences_controller.ex @@ -4,137 +4,29 @@ defmodule AtlasWeb.PreferencesController do alias Atlas.Accounts - def get_language(conn, _params) do + def get_preferences(conn, _params) do {user, _session} = Guardian.Plug.current_resource(conn) - language = Accounts.get_user_language(user.id) - json(conn, %{language: language}) + preferences = Accounts.get_user_preferences(user.id) + json(conn, %{ + user_id: preferences.user_id, + language: preferences.language + }) end - swagger_path :get_language do - get("/v1/auth/language") - summary("Get user language preference") - description("Returns the current language preference for the authenticated user.") - produces("application/json") - tag("Preferences") - operation_id("get_language") - response(200, "Language preference returned successfully", Schema.ref(:LanguageResponse)) - security([%{Bearer: []}]) - end - - def update_language(conn, %{"language" => language}) do + def get_preference(conn, %{"preference" => preference}) do {user, _session} = Guardian.Plug.current_resource(conn) - Accounts.set_user_language(user.id, language) - json(conn, %{language: language}) - end - - def update_language(conn, _params) do - conn - |> put_status(:bad_request) - |> json(%{error: "Language parameter is required"}) - end - - swagger_path :update_language do - post("/v1/auth/language") - summary("Update user language preference") - description("Updates the language preference for the authenticated user.") - produces("application/json") - consumes("application/json") - tag("Preferences") - operation_id("update_language") - - parameters do - language_request(:body, Schema.ref(:LanguageRequest), "Language preference to set", - required: true - ) + preference_value = Accounts.get_user_preference(user.id, preference) + case preference_value do + nil -> json(conn, %{error: "Preference not found"}) + _ -> json(conn, %{preference => preference_value}) end - - response(200, "Language preference updated successfully", Schema.ref(:LanguageResponse)) - response(400, "Bad request - Missing language parameter", Schema.ref(:ErrorResponse)) - security([%{Bearer: []}]) - end - - def available_languages(conn, _params) do - json(conn, %{languages: ["pt-PT", "en-US"]}) - end - - swagger_path :available_languages do - post("/v1/auth/available_languages") - summary("Get available languages") - description("Returns a list of all supported languages.") - produces("application/json") - tag("Preferences") - operation_id("available_languages") - - response( - 200, - "Available languages returned successfully", - Schema.ref(:AvailableLanguagesResponse) - ) - - security([%{Bearer: []}]) end - def swagger_definitions do - %{ - LanguageRequest: - swagger_schema do - title("LanguageRequest") - description("Request schema for updating language preference") - - properties do - language(:string, "Language", required: true, enum: ["pt-PT", "en-US"]) - end - - example(%{ - language: "en-US" - }) - end, - LanguageResponse: - swagger_schema do - title("LanguageResponse") - description("Response schema for language preference") - - properties do - language(:string, "Current language preference", - required: true, - enum: ["pt-PT", "en-US"] - ) - end - - example(%{ - language: "en-US" - }) - end, - AvailableLanguagesResponse: - swagger_schema do - title("AvailableLanguagesResponse") - description("Response schema for available languages") - - properties do - languages(:array, "List of supported language codes", - required: true, - items: %{type: :string} - ) - end - - example(%{ - languages: ["pt-PT", "en-US"] - }) - end, - ErrorResponse: - swagger_schema do - title("ErrorResponse") - description("Error response schema") - type(:object) - - properties do - error(:string, "Error message", required: true) - end - - example(%{ - error: "Language parameter is required" - }) - end - } + def update_preference(conn, %{"preference" => preference, "value" => value}) do + {user, _session} = Guardian.Plug.current_resource(conn) + case Accounts.set_user_preference(user.id, preference, value) do + {:ok, _} -> json(conn, %{status: "success", message: "Preference updated successfully"}) + {:error, _} -> json(conn, %{status: "error", message: "Invalid preference or value"}) + end end end diff --git a/lib/atlas_web/router.ex b/lib/atlas_web/router.ex index afef922..fb49b9e 100644 --- a/lib/atlas_web/router.ex +++ b/lib/atlas_web/router.ex @@ -44,9 +44,9 @@ defmodule AtlasWeb.Router do pipe_through :auth scope "/auth" do - get "/language", PreferencesController, :get_language - post "/language", PreferencesController, :update_language - post "/available_languages", PreferencesController, :available_languages + get "/preferences", PreferencesController, :get_preferences + get "/preferences/:preference", PreferencesController, :get_preference + put "/preferences/:preference", PreferencesController, :update_preference post "/sign_out", AuthController, :sign_out get "/me", AuthController, :me get "/sessions", AuthController, :sessions diff --git a/priv/static/swagger.json b/priv/static/swagger.json index 4de37f5..971693e 100644 --- a/priv/static/swagger.json +++ b/priv/static/swagger.json @@ -5,53 +5,6 @@ }, "host": "localhost:4000", "definitions": { - "Job": { - "description": "A job in the system", - "properties": { - "attempted_at": { - "description": "Timestamp when the job was attempted", - "format": "date-time", - "type": "string" - }, - "completed_at": { - "description": "Timestamp when the job was completed", - "format": "date-time", - "type": "string" - }, - "id": { - "description": "ID of the job", - "type": "integer" - }, - "inserted_at": { - "description": "Timestamp when the job was created", - "format": "date-time", - "type": "string" - }, - "state": { - "description": "Status of the job", - "type": "string" - }, - "type": { - "description": "Type of the job", - "type": "string" - }, - "user_id": { - "description": "ID of the user who created the job", - "type": "string" - } - }, - "required": [ - "completed_at", - "attempted_at", - "inserted_at", - "user_id", - "state", - "type", - "id" - ], - "title": "Job", - "type": "object" - }, "User": { "description": "User schema", "example": { @@ -141,6 +94,53 @@ "title": "User Session", "type": "object" }, + "Job": { + "description": "A job in the system", + "properties": { + "attempted_at": { + "description": "Timestamp when the job was attempted", + "format": "date-time", + "type": "string" + }, + "completed_at": { + "description": "Timestamp when the job was completed", + "format": "date-time", + "type": "string" + }, + "id": { + "description": "ID of the job", + "type": "integer" + }, + "inserted_at": { + "description": "Timestamp when the job was created", + "format": "date-time", + "type": "string" + }, + "state": { + "description": "Status of the job", + "type": "string" + }, + "type": { + "description": "Type of the job", + "type": "string" + }, + "user_id": { + "description": "ID of the user who created the job", + "type": "string" + } + }, + "required": [ + "completed_at", + "attempted_at", + "inserted_at", + "user_id", + "state", + "type", + "id" + ], + "title": "Job", + "type": "object" + }, "SignInResponse": { "description": "Response schema for successful sign in", "example": { @@ -180,9 +180,6 @@ }, "ErrorResponse": { "description": "Error response schema", - "example": { - "error": "Language parameter is required" - }, "properties": { "error": { "description": "Error message", @@ -333,101 +330,16 @@ }, "title": "JobResponse", "type": "object" - }, - "LanguageResponse": { - "description": "Response schema for language preference", - "example": { - "language": "en-US" - }, - "properties": { - "language": { - "description": "Current language preference", - "enum": [ - "pt-PT", - "en-US" - ], - "type": "string" - } - }, - "required": [ - "language" - ], - "title": "LanguageResponse", - "type": "object" - }, - "LanguageRequest": { - "description": "Request schema for updating language preference", - "example": { - "language": "en-US" - }, - "properties": { - "language": { - "description": "Language", - "enum": [ - "pt-PT", - "en-US" - ], - "type": "string" - } - }, - "required": [ - "language" - ], - "title": "LanguageRequest", - "type": "object" - }, - "AvailableLanguagesResponse": { - "description": "Response schema for available languages", - "example": { - "languages": [ - "pt-PT", - "en-US" - ] - }, - "properties": { - "languages": { - "description": "List of supported language codes", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "languages" - ], - "title": "AvailableLanguagesResponse", - "type": "object" + } + }, + "securityDefinitions": { + "Bearer": { + "in": "header", + "name": "Authorization", + "type": "apiKey" } }, "paths": { - "/v1/auth/available_languages": { - "post": { - "description": "Returns a list of all supported languages.", - "operationId": "available_languages", - "parameters": [], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "Available languages returned successfully", - "schema": { - "$ref": "#/definitions/AvailableLanguagesResponse" - } - } - }, - "security": [ - { - "Bearer": [] - } - ], - "summary": "Get available languages", - "tags": [ - "Preferences" - ] - } - }, "/v1/auth/forgot_password": { "post": { "description": "Sends password reset instructions to the user via email.", @@ -464,77 +376,6 @@ ] } }, - "/v1/auth/language": { - "get": { - "description": "Returns the current language preference for the authenticated user.", - "operationId": "get_language", - "parameters": [], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "Language preference returned successfully", - "schema": { - "$ref": "#/definitions/LanguageResponse" - } - } - }, - "security": [ - { - "Bearer": [] - } - ], - "summary": "Get user language preference", - "tags": [ - "Preferences" - ] - }, - "post": { - "consumes": [ - "application/json" - ], - "description": "Updates the language preference for the authenticated user.", - "operationId": "update_language", - "parameters": [ - { - "description": "Language preference to set", - "in": "body", - "name": "language_request", - "required": true, - "schema": { - "$ref": "#/definitions/LanguageRequest" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "Language preference updated successfully", - "schema": { - "$ref": "#/definitions/LanguageResponse" - } - }, - "400": { - "description": "Bad request - Missing language parameter", - "schema": { - "$ref": "#/definitions/ErrorResponse" - } - } - }, - "security": [ - { - "Bearer": [] - } - ], - "summary": "Update user language preference", - "tags": [ - "Preferences" - ] - } - }, "/v1/auth/me": { "get": { "description": "Returns the user in the current session.", @@ -865,12 +706,5 @@ } } }, - "swagger": "2.0", - "securityDefinitions": { - "Bearer": { - "in": "header", - "name": "Authorization", - "type": "apiKey" - } - } + "swagger": "2.0" } \ No newline at end of file diff --git a/test/atlas/preferences_test.exs b/test/atlas/preferences_test.exs new file mode 100644 index 0000000..4b4404d --- /dev/null +++ b/test/atlas/preferences_test.exs @@ -0,0 +1,44 @@ +defmodule Atlas.PreferencesTest do + use AtlasWeb.ConnCase + + alias AtlasWeb.PreferencesController + + setup do + conn = authenticated_conn(%{type: :student}) + {user, _session} = Guardian.Plug.current_resource(conn) + + %{ + authenticated_conn: conn, + user: user + } + end + + describe "get_preferences/2" do + test "returns the user's preferences", %{authenticated_conn: conn, user: user} do + conn = PreferencesController.get_preferences(conn, %{}) + + response = json_response(conn, 200) + assert response["user_id"] == user.id + assert response["language"] == "en-US" + end + end + + describe "get_preference/2" do + test "returns the specific preference (language)", %{authenticated_conn: conn} do + conn = PreferencesController.get_preference(conn, %{"preference" => "language"}) + response = json_response(conn, 200) + assert Map.has_key?(response, "language") + assert response["language"] == "en-US" + end + end + + describe "update_preference/2" do + test "updates the specific preference", %{authenticated_conn: conn} do + conn = PreferencesController.update_preference(conn, %{"preference" => "language", "value" => "pt-PT"}) + + response = json_response(conn, 200) + assert response["status"] == "success" + assert response["message"] == "Preference updated successfully" + end + end +end From f0b63f3600a528e255ca2baba247557992c9a600 Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Tue, 19 Aug 2025 22:35:28 +0100 Subject: [PATCH 3/6] format --- lib/atlas/accounts.ex | 2 +- .../controllers/preferences_controller.ex | 3 + priv/static/swagger.json | 204 +++++++++--------- test/atlas/preferences_test.exs | 6 +- 4 files changed, 111 insertions(+), 104 deletions(-) diff --git a/lib/atlas/accounts.ex b/lib/atlas/accounts.ex index 843dba8..f327be1 100644 --- a/lib/atlas/accounts.ex +++ b/lib/atlas/accounts.ex @@ -597,7 +597,7 @@ defmodule Atlas.Accounts do %UserPreference{} |> UserPreference.changeset(attrs) |> Repo.insert( - on_conflict: [set: [{preference, value}]], + on_conflict: [set: [{preference, value}]], conflict_target: :user_id ) end diff --git a/lib/atlas_web/controllers/preferences_controller.ex b/lib/atlas_web/controllers/preferences_controller.ex index 96e32d7..08a6731 100644 --- a/lib/atlas_web/controllers/preferences_controller.ex +++ b/lib/atlas_web/controllers/preferences_controller.ex @@ -7,6 +7,7 @@ defmodule AtlasWeb.PreferencesController do def get_preferences(conn, _params) do {user, _session} = Guardian.Plug.current_resource(conn) preferences = Accounts.get_user_preferences(user.id) + json(conn, %{ user_id: preferences.user_id, language: preferences.language @@ -16,6 +17,7 @@ defmodule AtlasWeb.PreferencesController do def get_preference(conn, %{"preference" => preference}) do {user, _session} = Guardian.Plug.current_resource(conn) preference_value = Accounts.get_user_preference(user.id, preference) + case preference_value do nil -> json(conn, %{error: "Preference not found"}) _ -> json(conn, %{preference => preference_value}) @@ -24,6 +26,7 @@ defmodule AtlasWeb.PreferencesController do def update_preference(conn, %{"preference" => preference, "value" => value}) do {user, _session} = Guardian.Plug.current_resource(conn) + case Accounts.set_user_preference(user.id, preference, value) do {:ok, _} -> json(conn, %{status: "success", message: "Preference updated successfully"}) {:error, _} -> json(conn, %{status: "error", message: "Invalid preference or value"}) diff --git a/priv/static/swagger.json b/priv/static/swagger.json index 971693e..fa9f7e3 100644 --- a/priv/static/swagger.json +++ b/priv/static/swagger.json @@ -94,74 +94,21 @@ "title": "User Session", "type": "object" }, - "Job": { - "description": "A job in the system", - "properties": { - "attempted_at": { - "description": "Timestamp when the job was attempted", - "format": "date-time", - "type": "string" - }, - "completed_at": { - "description": "Timestamp when the job was completed", - "format": "date-time", - "type": "string" - }, - "id": { - "description": "ID of the job", - "type": "integer" - }, - "inserted_at": { - "description": "Timestamp when the job was created", - "format": "date-time", - "type": "string" - }, - "state": { - "description": "Status of the job", - "type": "string" - }, - "type": { - "description": "Type of the job", - "type": "string" - }, - "user_id": { - "description": "ID of the user who created the job", - "type": "string" - } - }, - "required": [ - "completed_at", - "attempted_at", - "inserted_at", - "user_id", - "state", - "type", - "id" - ], - "title": "Job", - "type": "object" - }, - "SignInResponse": { - "description": "Response schema for successful sign in", + "SignOutResponse": { + "description": "Response schema for successful sign out", "example": { - "access_token": "xXxXxXxXxXxX", - "session_id": "e1387cae-ac1d-4aeb-8e13-ff1b3dd15ca4" + "message": "Signed out successfully" }, "properties": { - "access_token": { - "description": "Access token", + "message": { + "description": "Message indicating successful sign out", "type": "string" - }, - "session_id": { - "description": "User session ID", - "type": "integer" } }, "required": [ - "access_token", - "session_id" + "message" ], - "title": "SignInResponse", + "title": "SignOutResponse", "type": "object" }, "UnauthorizedResponse": { @@ -192,38 +139,27 @@ "title": "ErrorResponse", "type": "object" }, - "SuccessfulRefreshResponse": { - "description": "Response schema for successful token refresh", + "SignInResponse": { + "description": "Response schema for successful sign in", "example": { - "access_token": "xXxXxXxXxXxX" + "access_token": "xXxXxXxXxXxX", + "session_id": "e1387cae-ac1d-4aeb-8e13-ff1b3dd15ca4" }, "properties": { "access_token": { - "description": "New access token", - "type": "string" - } - }, - "required": [ - "access_token" - ], - "title": "SuccessfulRefreshResponse", - "type": "object" - }, - "SignOutResponse": { - "description": "Response schema for successful sign out", - "example": { - "message": "Signed out successfully" - }, - "properties": { - "message": { - "description": "Message indicating successful sign out", + "description": "Access token", "type": "string" + }, + "session_id": { + "description": "User session ID", + "type": "integer" } }, "required": [ - "message" + "access_token", + "session_id" ], - "title": "SignOutResponse", + "title": "SignInResponse", "type": "object" }, "UserSessionsResponse": { @@ -255,55 +191,100 @@ "title": "UserSessionsResponse", "type": "object" }, - "NoContentResponse": { - "description": "Response schema for no content", - "example": {}, + "ResetPasswordResponse": { + "description": "Response schema for successful password reset", + "example": { + "message": "Password reset successfully" + }, "properties": { "message": { - "description": "Message indicating no content", + "description": "Message indicating successful password reset", "type": "string" } }, "required": [ "message" ], - "title": "NoContentResponse", + "title": "ResetPasswordResponse", "type": "object" }, - "ResetPasswordResponse": { - "description": "Response schema for successful password reset", + "SuccessfulRefreshResponse": { + "description": "Response schema for successful token refresh", "example": { - "message": "Password reset successfully" + "access_token": "xXxXxXxXxXxX" }, + "properties": { + "access_token": { + "description": "New access token", + "type": "string" + } + }, + "required": [ + "access_token" + ], + "title": "SuccessfulRefreshResponse", + "type": "object" + }, + "NoContentResponse": { + "description": "Response schema for no content", + "example": {}, "properties": { "message": { - "description": "Message indicating successful password reset", + "description": "Message indicating no content", "type": "string" } }, "required": [ "message" ], - "title": "ResetPasswordResponse", + "title": "NoContentResponse", "type": "object" }, - "SuccessfulImport": { - "description": "Response for a successful import", + "Job": { + "description": "A job in the system", "properties": { - "job_id": { - "description": "ID of the import job", + "attempted_at": { + "description": "Timestamp when the job was attempted", + "format": "date-time", "type": "string" }, - "message": { - "description": "Status message", + "completed_at": { + "description": "Timestamp when the job was completed", + "format": "date-time", + "type": "string" + }, + "id": { + "description": "ID of the job", + "type": "integer" + }, + "inserted_at": { + "description": "Timestamp when the job was created", + "format": "date-time", + "type": "string" + }, + "state": { + "description": "Status of the job", + "type": "string" + }, + "type": { + "description": "Type of the job", + "type": "string" + }, + "user_id": { + "description": "ID of the user who created the job", "type": "string" } }, "required": [ - "message", - "job_id" + "completed_at", + "attempted_at", + "inserted_at", + "user_id", + "state", + "type", + "id" ], - "title": "SuccessfulImport", + "title": "Job", "type": "object" }, "JobsResponse": { @@ -330,6 +311,25 @@ }, "title": "JobResponse", "type": "object" + }, + "SuccessfulImport": { + "description": "Response for a successful import", + "properties": { + "job_id": { + "description": "ID of the import job", + "type": "string" + }, + "message": { + "description": "Status message", + "type": "string" + } + }, + "required": [ + "message", + "job_id" + ], + "title": "SuccessfulImport", + "type": "object" } }, "securityDefinitions": { diff --git a/test/atlas/preferences_test.exs b/test/atlas/preferences_test.exs index 4b4404d..0cc1ce2 100644 --- a/test/atlas/preferences_test.exs +++ b/test/atlas/preferences_test.exs @@ -34,7 +34,11 @@ defmodule Atlas.PreferencesTest do describe "update_preference/2" do test "updates the specific preference", %{authenticated_conn: conn} do - conn = PreferencesController.update_preference(conn, %{"preference" => "language", "value" => "pt-PT"}) + conn = + PreferencesController.update_preference(conn, %{ + "preference" => "language", + "value" => "pt-PT" + }) response = json_response(conn, 200) assert response["status"] == "success" From 9868fa3dcf50aa100b0b6f0f659156eab284f51d Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Fri, 22 Aug 2025 01:21:39 +0100 Subject: [PATCH 4/6] fix: schema and update_preferences function --- lib/atlas/accounts.ex | 73 +++++-- lib/atlas/accounts/user_preferences.ex | 11 +- .../controllers/preferences_controller.ex | 18 +- lib/atlas_web/router.ex | 3 +- ...20250726211654_create_user_preferences.exs | 11 +- priv/static/swagger.json | 204 +++++++++--------- test/atlas/preferences_test.exs | 56 ++++- 7 files changed, 232 insertions(+), 144 deletions(-) diff --git a/lib/atlas/accounts.ex b/lib/atlas/accounts.ex index f327be1..9742820 100644 --- a/lib/atlas/accounts.ex +++ b/lib/atlas/accounts.ex @@ -548,6 +548,28 @@ defmodule Atlas.Accounts do ## User Preferences + @doc """ + Creates a user preference. + """ + def create_preference(attrs) when is_map(attrs) and map_size(attrs) > 0 do + %UserPreference{} + |> UserPreference.changeset(attrs) + |> Repo.insert() + end + + def create_preference(_), do: {:error, :invalid_fields} + + @doc """ + Updates a user preference. + """ + def update_preference(%UserPreference{} = preference, attrs) when is_map(attrs) and map_size(attrs) > 0 do + preference + |> UserPreference.changeset(attrs) + |> Repo.update() + end + + def update_preference(_, _), do: {:error, :invalid_fields} + @doc """ Gets the set of preferences of a given user. @@ -580,35 +602,48 @@ defmodule Atlas.Accounts do end @doc """ - Sets a given preference for a user. + Sets a given preference or preferences for an user. ## Examples - iex> set_user_preference(1, "language", "en-US") - {:ok, %UserPreference{}} + iex> set_user_preference(%{"user_id" => "1", "language" => "en-US"}) + %UserPreference{} - iex> set_user_preference(1, "void", "none") - {:error, %Ecto.Changeset{}} + iex> set_user_preference(%{"user_id" => "1", "language" => "pt-PT", "invalid_field" => "none"}) + %UserPreference{} + + iex> set_user_preference(%{"user_id" => "2", "language" => nil}) + {:error, :invalid_fields} + + iex> set_user_preference(%{"user_id" => "2", %{}}) + {:error, :invalid_fields} """ - def set_user_preference(user_id, preference, value) do - preference = String.to_atom(preference) - attrs = %{preference => value, user_id: user_id} - %UserPreference{} - |> UserPreference.changeset(attrs) - |> Repo.insert( - on_conflict: [set: [{preference, value}]], - conflict_target: :user_id - ) + def set_user_preference(%{"user_id" => user_id} = attrs) when is_binary(user_id) do + ap = get_available_preferences() + + update_fields = + attrs + |> Enum.reject(fn {k, v} -> is_nil(v) or not (k in ap) end) + |> Enum.map(fn {k, v} -> {String.to_atom(k), v} end) + |> Enum.into(%{}) + + case get_user_preferences(user_id) do + nil -> create_preference(update_fields) + %UserPreference{} = up -> update_preference(up, update_fields) + end end + @doc """ + Gets the available user preferences. + """ + def get_available_preferences(), do: ["language"] + + @doc false defp create_default_preferences_multi(multi) do Ecto.Multi.insert(multi, :preferences, fn %{user: user} -> - %UserPreference{} - |> UserPreference.changeset(%{ - user_id: user.id, - language: "en-US" - }) + %UserPreference{user_id: user.id} + |> UserPreference.changeset(%{}) end) end end diff --git a/lib/atlas/accounts/user_preferences.ex b/lib/atlas/accounts/user_preferences.ex index ada33a3..6c1ca66 100644 --- a/lib/atlas/accounts/user_preferences.ex +++ b/lib/atlas/accounts/user_preferences.ex @@ -3,10 +3,11 @@ defmodule Atlas.Accounts.UserPreference do Schema for storing a user's preference. """ - use Ecto.Schema - import Ecto.Changeset + use Atlas.Schema @languages ~w(pt-PT en-US) + @optional_fields ~w(language)a + @required_fields ~w(user_id)a schema "user_preferences" do field :language, :string @@ -18,10 +19,8 @@ defmodule Atlas.Accounts.UserPreference do def changeset(user_preference, attrs) do user_preference - |> cast(attrs, [:user_id, :language]) - |> validate_required([:user_id, :language]) + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) |> validate_inclusion(:language, @languages) - |> assoc_constraint(:user) - |> unique_constraint(:user_id) end end diff --git a/lib/atlas_web/controllers/preferences_controller.ex b/lib/atlas_web/controllers/preferences_controller.ex index 08a6731..2869be3 100644 --- a/lib/atlas_web/controllers/preferences_controller.ex +++ b/lib/atlas_web/controllers/preferences_controller.ex @@ -24,12 +24,22 @@ defmodule AtlasWeb.PreferencesController do end end - def update_preference(conn, %{"preference" => preference, "value" => value}) do + def update_preferences(conn, attrs) do {user, _session} = Guardian.Plug.current_resource(conn) + case Accounts.set_user_preference(Map.put(attrs, "user_id", user.id)) do + {:ok, _} -> + json(conn, %{status: "success", message: "Preferences updated successfully"}) - case Accounts.set_user_preference(user.id, preference, value) do - {:ok, _} -> json(conn, %{status: "success", message: "Preference updated successfully"}) - {:error, _} -> json(conn, %{status: "error", message: "Invalid preference or value"}) + {:error, :invalid_fields} -> + json(conn, %{status: "error", message: "No valid fields provided"}) + + {:error, _changeset} -> + json(conn, %{status: "error", message: "No valid values provided"}) end end + + def get_available_preferences(conn, _params) do + preferences = Accounts.get_available_preferences() + json(conn, %{preferences: preferences}) + end end diff --git a/lib/atlas_web/router.ex b/lib/atlas_web/router.ex index fb49b9e..2d33e30 100644 --- a/lib/atlas_web/router.ex +++ b/lib/atlas_web/router.ex @@ -46,7 +46,8 @@ defmodule AtlasWeb.Router do scope "/auth" do get "/preferences", PreferencesController, :get_preferences get "/preferences/:preference", PreferencesController, :get_preference - put "/preferences/:preference", PreferencesController, :update_preference + get "/available_preferences", PreferencesController, :get_available_preferences + put "/preferences", PreferencesController, :update_preferences post "/sign_out", AuthController, :sign_out get "/me", AuthController, :me get "/sessions", AuthController, :sessions diff --git a/priv/repo/migrations/20250726211654_create_user_preferences.exs b/priv/repo/migrations/20250726211654_create_user_preferences.exs index ff7a4c4..96679bb 100644 --- a/priv/repo/migrations/20250726211654_create_user_preferences.exs +++ b/priv/repo/migrations/20250726211654_create_user_preferences.exs @@ -2,14 +2,15 @@ defmodule Atlas.Repo.Migrations.CreateUserPreferences do use Ecto.Migration def change do - create table(:user_preferences) do - add :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false - add :language, :string, null: false + create table(:user_preferences, primary_key: false) do + add :id, :binary_id, primary_key: true + add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false + add :language, :string - timestamps() + timestamps(type: :utc_datetime) end - create unique_index(:user_preferences, [:user_id]) + create index(:user_preferences, [:user_id]) create constraint(:user_preferences, :language_must_be_valid, check: "language IN ('pt-PT', 'en-US')" diff --git a/priv/static/swagger.json b/priv/static/swagger.json index fa9f7e3..971693e 100644 --- a/priv/static/swagger.json +++ b/priv/static/swagger.json @@ -94,21 +94,74 @@ "title": "User Session", "type": "object" }, - "SignOutResponse": { - "description": "Response schema for successful sign out", + "Job": { + "description": "A job in the system", + "properties": { + "attempted_at": { + "description": "Timestamp when the job was attempted", + "format": "date-time", + "type": "string" + }, + "completed_at": { + "description": "Timestamp when the job was completed", + "format": "date-time", + "type": "string" + }, + "id": { + "description": "ID of the job", + "type": "integer" + }, + "inserted_at": { + "description": "Timestamp when the job was created", + "format": "date-time", + "type": "string" + }, + "state": { + "description": "Status of the job", + "type": "string" + }, + "type": { + "description": "Type of the job", + "type": "string" + }, + "user_id": { + "description": "ID of the user who created the job", + "type": "string" + } + }, + "required": [ + "completed_at", + "attempted_at", + "inserted_at", + "user_id", + "state", + "type", + "id" + ], + "title": "Job", + "type": "object" + }, + "SignInResponse": { + "description": "Response schema for successful sign in", "example": { - "message": "Signed out successfully" + "access_token": "xXxXxXxXxXxX", + "session_id": "e1387cae-ac1d-4aeb-8e13-ff1b3dd15ca4" }, "properties": { - "message": { - "description": "Message indicating successful sign out", + "access_token": { + "description": "Access token", "type": "string" + }, + "session_id": { + "description": "User session ID", + "type": "integer" } }, "required": [ - "message" + "access_token", + "session_id" ], - "title": "SignOutResponse", + "title": "SignInResponse", "type": "object" }, "UnauthorizedResponse": { @@ -139,27 +192,38 @@ "title": "ErrorResponse", "type": "object" }, - "SignInResponse": { - "description": "Response schema for successful sign in", + "SuccessfulRefreshResponse": { + "description": "Response schema for successful token refresh", "example": { - "access_token": "xXxXxXxXxXxX", - "session_id": "e1387cae-ac1d-4aeb-8e13-ff1b3dd15ca4" + "access_token": "xXxXxXxXxXxX" }, "properties": { "access_token": { - "description": "Access token", + "description": "New access token", "type": "string" - }, - "session_id": { - "description": "User session ID", - "type": "integer" } }, "required": [ - "access_token", - "session_id" + "access_token" ], - "title": "SignInResponse", + "title": "SuccessfulRefreshResponse", + "type": "object" + }, + "SignOutResponse": { + "description": "Response schema for successful sign out", + "example": { + "message": "Signed out successfully" + }, + "properties": { + "message": { + "description": "Message indicating successful sign out", + "type": "string" + } + }, + "required": [ + "message" + ], + "title": "SignOutResponse", "type": "object" }, "UserSessionsResponse": { @@ -191,100 +255,55 @@ "title": "UserSessionsResponse", "type": "object" }, - "ResetPasswordResponse": { - "description": "Response schema for successful password reset", - "example": { - "message": "Password reset successfully" - }, + "NoContentResponse": { + "description": "Response schema for no content", + "example": {}, "properties": { "message": { - "description": "Message indicating successful password reset", + "description": "Message indicating no content", "type": "string" } }, "required": [ "message" ], - "title": "ResetPasswordResponse", + "title": "NoContentResponse", "type": "object" }, - "SuccessfulRefreshResponse": { - "description": "Response schema for successful token refresh", + "ResetPasswordResponse": { + "description": "Response schema for successful password reset", "example": { - "access_token": "xXxXxXxXxXxX" + "message": "Password reset successfully" }, - "properties": { - "access_token": { - "description": "New access token", - "type": "string" - } - }, - "required": [ - "access_token" - ], - "title": "SuccessfulRefreshResponse", - "type": "object" - }, - "NoContentResponse": { - "description": "Response schema for no content", - "example": {}, "properties": { "message": { - "description": "Message indicating no content", + "description": "Message indicating successful password reset", "type": "string" } }, "required": [ "message" ], - "title": "NoContentResponse", + "title": "ResetPasswordResponse", "type": "object" }, - "Job": { - "description": "A job in the system", + "SuccessfulImport": { + "description": "Response for a successful import", "properties": { - "attempted_at": { - "description": "Timestamp when the job was attempted", - "format": "date-time", - "type": "string" - }, - "completed_at": { - "description": "Timestamp when the job was completed", - "format": "date-time", - "type": "string" - }, - "id": { - "description": "ID of the job", - "type": "integer" - }, - "inserted_at": { - "description": "Timestamp when the job was created", - "format": "date-time", - "type": "string" - }, - "state": { - "description": "Status of the job", - "type": "string" - }, - "type": { - "description": "Type of the job", + "job_id": { + "description": "ID of the import job", "type": "string" }, - "user_id": { - "description": "ID of the user who created the job", + "message": { + "description": "Status message", "type": "string" } }, "required": [ - "completed_at", - "attempted_at", - "inserted_at", - "user_id", - "state", - "type", - "id" + "message", + "job_id" ], - "title": "Job", + "title": "SuccessfulImport", "type": "object" }, "JobsResponse": { @@ -311,25 +330,6 @@ }, "title": "JobResponse", "type": "object" - }, - "SuccessfulImport": { - "description": "Response for a successful import", - "properties": { - "job_id": { - "description": "ID of the import job", - "type": "string" - }, - "message": { - "description": "Status message", - "type": "string" - } - }, - "required": [ - "message", - "job_id" - ], - "title": "SuccessfulImport", - "type": "object" } }, "securityDefinitions": { diff --git a/test/atlas/preferences_test.exs b/test/atlas/preferences_test.exs index 0cc1ce2..af29a66 100644 --- a/test/atlas/preferences_test.exs +++ b/test/atlas/preferences_test.exs @@ -19,30 +19,72 @@ defmodule Atlas.PreferencesTest do response = json_response(conn, 200) assert response["user_id"] == user.id - assert response["language"] == "en-US" + assert Map.has_key?(response, "language") end end describe "get_preference/2" do test "returns the specific preference (language)", %{authenticated_conn: conn} do + + PreferencesController.update_preferences(conn, %{ + "language" => "en-US" + }) + conn = PreferencesController.get_preference(conn, %{"preference" => "language"}) + response = json_response(conn, 200) assert Map.has_key?(response, "language") assert response["language"] == "en-US" end + + test "returns error for non-existent preference", %{authenticated_conn: conn} do + conn = PreferencesController.get_preference(conn, %{"preference" => "none"}) + + response = json_response(conn, 200) + assert response["error"] == "Preference not found" + end end describe "update_preference/2" do test "updates the specific preference", %{authenticated_conn: conn} do - conn = - PreferencesController.update_preference(conn, %{ - "preference" => "language", - "value" => "pt-PT" - }) + conn = PreferencesController.update_preferences(conn, %{ + "language" => "pt-PT" + }) response = json_response(conn, 200) assert response["status"] == "success" - assert response["message"] == "Preference updated successfully" + assert response["message"] == "Preferences updated successfully" + end + + test "returns error for invalid fields", %{authenticated_conn: conn} do + conn = PreferencesController.update_preferences(conn, %{ + "invalid_field" => "value" + }) + + response = json_response(conn, 200) + assert response["status"] == "error" + assert response["message"] == "No valid fields provided" + end + + test "returns error for invalid values", %{authenticated_conn: conn} do + conn = PreferencesController.update_preferences(conn, %{ + "language" => "invalid_value" + }) + + response = json_response(conn, 200) + assert response["status"] == "error" + assert response["message"] == "No valid values provided" + end + end + + describe "get_available_preferences/2" do + test "returns available preferences", %{authenticated_conn: conn} do + conn = PreferencesController.get_available_preferences(conn, %{}) + + response = json_response(conn, 200) + assert Map.has_key?(response, "preferences") + assert is_list(response["preferences"]) + assert "language" in response["preferences"] end end end From 0a426e468e26ff2259ed0dac5ac7bdfef718a113 Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Fri, 22 Aug 2025 01:22:47 +0100 Subject: [PATCH 5/6] format --- lib/atlas/accounts.ex | 13 ++++++----- .../controllers/preferences_controller.ex | 1 + test/atlas/preferences_test.exs | 22 ++++++++++--------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/atlas/accounts.ex b/lib/atlas/accounts.ex index 9742820..b213a62 100644 --- a/lib/atlas/accounts.ex +++ b/lib/atlas/accounts.ex @@ -562,7 +562,8 @@ defmodule Atlas.Accounts do @doc """ Updates a user preference. """ - def update_preference(%UserPreference{} = preference, attrs) when is_map(attrs) and map_size(attrs) > 0 do + def update_preference(%UserPreference{} = preference, attrs) + when is_map(attrs) and map_size(attrs) > 0 do preference |> UserPreference.changeset(attrs) |> Repo.update() @@ -623,10 +624,10 @@ defmodule Atlas.Accounts do ap = get_available_preferences() update_fields = - attrs - |> Enum.reject(fn {k, v} -> is_nil(v) or not (k in ap) end) - |> Enum.map(fn {k, v} -> {String.to_atom(k), v} end) - |> Enum.into(%{}) + attrs + |> Enum.reject(fn {k, v} -> is_nil(v) or k not in ap end) + |> Enum.map(fn {k, v} -> {String.to_atom(k), v} end) + |> Enum.into(%{}) case get_user_preferences(user_id) do nil -> create_preference(update_fields) @@ -637,7 +638,7 @@ defmodule Atlas.Accounts do @doc """ Gets the available user preferences. """ - def get_available_preferences(), do: ["language"] + def get_available_preferences, do: ["language"] @doc false defp create_default_preferences_multi(multi) do diff --git a/lib/atlas_web/controllers/preferences_controller.ex b/lib/atlas_web/controllers/preferences_controller.ex index 2869be3..1978ee9 100644 --- a/lib/atlas_web/controllers/preferences_controller.ex +++ b/lib/atlas_web/controllers/preferences_controller.ex @@ -26,6 +26,7 @@ defmodule AtlasWeb.PreferencesController do def update_preferences(conn, attrs) do {user, _session} = Guardian.Plug.current_resource(conn) + case Accounts.set_user_preference(Map.put(attrs, "user_id", user.id)) do {:ok, _} -> json(conn, %{status: "success", message: "Preferences updated successfully"}) diff --git a/test/atlas/preferences_test.exs b/test/atlas/preferences_test.exs index af29a66..5f19fe6 100644 --- a/test/atlas/preferences_test.exs +++ b/test/atlas/preferences_test.exs @@ -25,7 +25,6 @@ defmodule Atlas.PreferencesTest do describe "get_preference/2" do test "returns the specific preference (language)", %{authenticated_conn: conn} do - PreferencesController.update_preferences(conn, %{ "language" => "en-US" }) @@ -47,9 +46,10 @@ defmodule Atlas.PreferencesTest do describe "update_preference/2" do test "updates the specific preference", %{authenticated_conn: conn} do - conn = PreferencesController.update_preferences(conn, %{ - "language" => "pt-PT" - }) + conn = + PreferencesController.update_preferences(conn, %{ + "language" => "pt-PT" + }) response = json_response(conn, 200) assert response["status"] == "success" @@ -57,9 +57,10 @@ defmodule Atlas.PreferencesTest do end test "returns error for invalid fields", %{authenticated_conn: conn} do - conn = PreferencesController.update_preferences(conn, %{ - "invalid_field" => "value" - }) + conn = + PreferencesController.update_preferences(conn, %{ + "invalid_field" => "value" + }) response = json_response(conn, 200) assert response["status"] == "error" @@ -67,9 +68,10 @@ defmodule Atlas.PreferencesTest do end test "returns error for invalid values", %{authenticated_conn: conn} do - conn = PreferencesController.update_preferences(conn, %{ - "language" => "invalid_value" - }) + conn = + PreferencesController.update_preferences(conn, %{ + "language" => "invalid_value" + }) response = json_response(conn, 200) assert response["status"] == "error" From b037adc49b847d564e22a276473c4b80f6c00e08 Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Thu, 28 Aug 2025 13:30:09 +0100 Subject: [PATCH 6/6] fix: remove swagger import --- lib/atlas_web/controllers/preferences_controller.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/atlas_web/controllers/preferences_controller.ex b/lib/atlas_web/controllers/preferences_controller.ex index 1978ee9..c593b0f 100644 --- a/lib/atlas_web/controllers/preferences_controller.ex +++ b/lib/atlas_web/controllers/preferences_controller.ex @@ -1,6 +1,5 @@ defmodule AtlasWeb.PreferencesController do use AtlasWeb, :controller - use PhoenixSwagger alias Atlas.Accounts