From b4d8611c7340ddde42793b71b2819e531f8a0568 Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Mon, 7 Jul 2025 19:23:08 +0100 Subject: [PATCH 01/10] feat: swagger & swagger-ui --- config/config.exs | 12 ++++ .../controllers/swagger_controller.ex | 55 +++++++++++++++++++ lib/atlas_web/controllers/test_controller.ex | 29 ++++++++++ lib/atlas_web/router.ex | 19 +++++++ mix.exs | 7 ++- mix.lock | 1 + priv/static/swagger.json | 48 ++++++++++++++++ 7 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 lib/atlas_web/controllers/swagger_controller.ex create mode 100644 lib/atlas_web/controllers/test_controller.ex create mode 100644 priv/static/swagger.json diff --git a/config/config.exs b/config/config.exs index e57137c..4db6f9c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -22,6 +22,15 @@ config :atlas, AtlasWeb.Endpoint, pubsub_server: Atlas.PubSub, live_view: [signing_salt: "Gt4Lm9lT"] +# Configures the Swagger +config :atlas, :phoenix_swagger, +swagger_files: %{ + "priv/static/swagger.json" => [ + router: AtlasWeb.Router, + endpoint: AtlasWeb.Endpoint + ] +} + # Configures the mailer # # By default it uses the "Local" adapter which stores the emails @@ -39,6 +48,9 @@ config :logger, :console, # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason +# Use Jason for JSON parsing in Phoenix Swagger +config :phoenix_swagger, json_library: Jason + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/lib/atlas_web/controllers/swagger_controller.ex b/lib/atlas_web/controllers/swagger_controller.ex new file mode 100644 index 0000000..6a58d25 --- /dev/null +++ b/lib/atlas_web/controllers/swagger_controller.ex @@ -0,0 +1,55 @@ +defmodule AtlasWeb.SwaggerController do + use AtlasWeb, :controller + + def swagger(conn, _params) do + swagger_path = Path.join(:code.priv_dir(:atlas), "static/swagger.json") + json = File.read!(swagger_path) + + conn + |> put_resp_content_type("application/json") + |> send_resp(200, json) + end + + def swagger_ui(conn, _params) do + html_content = """ + + + + Atlas API Documentation + + + + +
+ + + + + + """ + + conn + |> put_resp_content_type("text/html") + |> send_resp(200, html_content) + end +end diff --git a/lib/atlas_web/controllers/test_controller.ex b/lib/atlas_web/controllers/test_controller.ex new file mode 100644 index 0000000..07cea55 --- /dev/null +++ b/lib/atlas_web/controllers/test_controller.ex @@ -0,0 +1,29 @@ +defmodule AtlasWeb.TestController do + use AtlasWeb, :controller + use PhoenixSwagger + + swagger_path :index do + get("/api/test") + summary("Health check") + description("Returns a simple OK message.") + produces("application/json") + response(200, "Success", Schema.ref(:HealthResponse)) + end + + def index(conn, _params) do + json(conn, %{message: "ok"}) + end + + def swagger_definitions do + %{ + HealthResponse: swagger_schema do + title("HealthCheck") + description("Simple response to confirm service is alive") + properties do + message(:string, "Confirmation message") + end + example(%{message: "ok"}) + end + } + end +end diff --git a/lib/atlas_web/router.ex b/lib/atlas_web/router.ex index e51cc9e..8f89762 100644 --- a/lib/atlas_web/router.ex +++ b/lib/atlas_web/router.ex @@ -7,8 +7,18 @@ defmodule AtlasWeb.Router do scope "/api", AtlasWeb do pipe_through :api + + get "/test", TestController, :index + end + + scope "/", AtlasWeb do + pipe_through :api + + get "/swagger.json", SwaggerController, :swagger + get "/swagger", SwaggerController, :swagger_ui end + # Enable LiveDashboard and Swoosh mailbox preview in development if Application.compile_env(:atlas, :dev_routes) do # If you want to use the LiveDashboard in production, you should put @@ -25,4 +35,13 @@ defmodule AtlasWeb.Router do forward "/mailbox", Plug.Swoosh.MailboxPreview end end + + def swagger_info do + %{ + info: %{ + version: "0.1.0", + title: "Atlas" + } + } + end end diff --git a/mix.exs b/mix.exs index b6d2366..4b15f77 100644 --- a/mix.exs +++ b/mix.exs @@ -9,7 +9,7 @@ defmodule Atlas.MixProject do elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, aliases: aliases(), - deps: deps() + deps: deps(), ] end @@ -55,7 +55,10 @@ defmodule Atlas.MixProject do # server {:dns_cluster, "~> 0.1.1"}, - {:bandit, "~> 1.2"} + {:bandit, "~> 1.2"}, + + # swagger + {:phoenix_swagger, "~> 0.8", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index fa8a2f8..c164fc2 100644 --- a/mix.lock +++ b/mix.lock @@ -22,6 +22,7 @@ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.17", "beeb16d83a7d3760f7ad463df94e83b087577665d2acc0bf2987cd7d9778068f", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a4ca05c1eb6922c4d07a508a75bfa12c45e5f4d8f77ae83283465f02c53741e1"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_swagger": {:hex, :phoenix_swagger, "0.8.3", "298d6204802409d3b0b4fc1013873839478707cf3a62532a9e10fec0e26d0e37", [:mix], [{:ex_json_schema, "~> 0.7.1", [hex: :ex_json_schema, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.11", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "3bc0fa9f5b679b8a61b90a52b2c67dd932320e9a84a6f91a4af872a0ab367337"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, diff --git a/priv/static/swagger.json b/priv/static/swagger.json new file mode 100644 index 0000000..afbe819 --- /dev/null +++ b/priv/static/swagger.json @@ -0,0 +1,48 @@ +{ + "info": { + "version": "0.1.0", + "title": "Atlas" + }, + "host": "localhost:4000", + "definitions": { + "HealthResponse": { + "description": "Simple response to confirm service is alive", + "example": { + "message": "ok" + }, + "properties": { + "message": { + "description": "Confirmation message", + "type": "string" + } + }, + "title": "HealthCheck", + "type": "object" + } + }, + "paths": { + "/api/test": { + "get": { + "description": "Returns a simple OK message.", + "operationId": "AtlasWeb.TestController.index", + "parameters": [], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/HealthResponse" + } + } + }, + "summary": "Health check", + "tags": [ + "Test" + ] + } + } + }, + "swagger": "2.0" +} \ No newline at end of file From 3c9043a4a908f97524c291a45fe07b207f34081a Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Mon, 7 Jul 2025 19:36:57 +0100 Subject: [PATCH 02/10] format --- config/config.exs | 12 ++++++------ lib/atlas_web/controllers/test_controller.ex | 17 ++++++++++------- lib/atlas_web/router.ex | 1 - mix.exs | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/config/config.exs b/config/config.exs index 4db6f9c..7c8d222 100644 --- a/config/config.exs +++ b/config/config.exs @@ -24,12 +24,12 @@ config :atlas, AtlasWeb.Endpoint, # Configures the Swagger config :atlas, :phoenix_swagger, -swagger_files: %{ - "priv/static/swagger.json" => [ - router: AtlasWeb.Router, - endpoint: AtlasWeb.Endpoint - ] -} + swagger_files: %{ + "priv/static/swagger.json" => [ + router: AtlasWeb.Router, + endpoint: AtlasWeb.Endpoint + ] + } # Configures the mailer # diff --git a/lib/atlas_web/controllers/test_controller.ex b/lib/atlas_web/controllers/test_controller.ex index 07cea55..4de95aa 100644 --- a/lib/atlas_web/controllers/test_controller.ex +++ b/lib/atlas_web/controllers/test_controller.ex @@ -16,14 +16,17 @@ defmodule AtlasWeb.TestController do def swagger_definitions do %{ - HealthResponse: swagger_schema do - title("HealthCheck") - description("Simple response to confirm service is alive") - properties do - message(:string, "Confirmation message") + HealthResponse: + swagger_schema do + title("HealthCheck") + description("Simple response to confirm service is alive") + + properties do + message(:string, "Confirmation message") + end + + example(%{message: "ok"}) end - example(%{message: "ok"}) - end } end end diff --git a/lib/atlas_web/router.ex b/lib/atlas_web/router.ex index 8f89762..93e67c6 100644 --- a/lib/atlas_web/router.ex +++ b/lib/atlas_web/router.ex @@ -18,7 +18,6 @@ defmodule AtlasWeb.Router do get "/swagger", SwaggerController, :swagger_ui end - # Enable LiveDashboard and Swoosh mailbox preview in development if Application.compile_env(:atlas, :dev_routes) do # If you want to use the LiveDashboard in production, you should put diff --git a/mix.exs b/mix.exs index 4b15f77..b504079 100644 --- a/mix.exs +++ b/mix.exs @@ -9,7 +9,7 @@ defmodule Atlas.MixProject do elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, aliases: aliases(), - deps: deps(), + deps: deps() ] end From d2e9ea64d13705f1bb781292ad01d5cba72d510e Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Fri, 11 Jul 2025 12:20:14 +0100 Subject: [PATCH 03/10] fix: review --- .../controllers/swagger_controller.ex | 55 ------------------- lib/atlas_web/endpoint.ex | 2 +- lib/atlas_web/router.ex | 9 ++- 3 files changed, 5 insertions(+), 61 deletions(-) delete mode 100644 lib/atlas_web/controllers/swagger_controller.ex diff --git a/lib/atlas_web/controllers/swagger_controller.ex b/lib/atlas_web/controllers/swagger_controller.ex deleted file mode 100644 index 6a58d25..0000000 --- a/lib/atlas_web/controllers/swagger_controller.ex +++ /dev/null @@ -1,55 +0,0 @@ -defmodule AtlasWeb.SwaggerController do - use AtlasWeb, :controller - - def swagger(conn, _params) do - swagger_path = Path.join(:code.priv_dir(:atlas), "static/swagger.json") - json = File.read!(swagger_path) - - conn - |> put_resp_content_type("application/json") - |> send_resp(200, json) - end - - def swagger_ui(conn, _params) do - html_content = """ - - - - Atlas API Documentation - - - - -
- - - - - - """ - - conn - |> put_resp_content_type("text/html") - |> send_resp(200, html_content) - end -end diff --git a/lib/atlas_web/endpoint.ex b/lib/atlas_web/endpoint.ex index fb8245a..49dbb5b 100644 --- a/lib/atlas_web/endpoint.ex +++ b/lib/atlas_web/endpoint.ex @@ -23,7 +23,7 @@ defmodule AtlasWeb.Endpoint do at: "/", from: :atlas, gzip: false, - only: AtlasWeb.static_paths() + only: ~w(css fonts images js favicon.ico robots.txt swagger.json) # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. diff --git a/lib/atlas_web/router.ex b/lib/atlas_web/router.ex index 93e67c6..e738cdd 100644 --- a/lib/atlas_web/router.ex +++ b/lib/atlas_web/router.ex @@ -1,8 +1,10 @@ defmodule AtlasWeb.Router do use AtlasWeb, :router + alias PhoenixSwagger.Plug.Validate pipeline :api do plug :accepts, ["json"] + plug(Validate, validation_failed_status: 422) end scope "/api", AtlasWeb do @@ -11,11 +13,8 @@ defmodule AtlasWeb.Router do get "/test", TestController, :index end - scope "/", AtlasWeb do - pipe_through :api - - get "/swagger.json", SwaggerController, :swagger - get "/swagger", SwaggerController, :swagger_ui + scope "/swagger" do + forward("/", PhoenixSwagger.Plug.SwaggerUI, otp_app: :atlas, swagger_file: "swagger.json") end # Enable LiveDashboard and Swoosh mailbox preview in development From 7f31c4a06711a88c5bbc60069f9926714e725620 Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Fri, 11 Jul 2025 13:37:27 +0100 Subject: [PATCH 04/10] fix: remove test paths --- lib/atlas_web.ex | 2 +- lib/atlas_web/controllers/test_controller.ex | 32 --------------- lib/atlas_web/endpoint.ex | 2 +- priv/static/swagger.json | 42 +------------------- 4 files changed, 4 insertions(+), 74 deletions(-) delete mode 100644 lib/atlas_web/controllers/test_controller.ex diff --git a/lib/atlas_web.ex b/lib/atlas_web.ex index cf02bfb..80145d1 100644 --- a/lib/atlas_web.ex +++ b/lib/atlas_web.ex @@ -17,7 +17,7 @@ defmodule AtlasWeb do those modules here. """ - def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) + def static_paths, do: ~w(assets fonts images favicon.ico robots.txt swagger.json) def router do quote do diff --git a/lib/atlas_web/controllers/test_controller.ex b/lib/atlas_web/controllers/test_controller.ex deleted file mode 100644 index 4de95aa..0000000 --- a/lib/atlas_web/controllers/test_controller.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule AtlasWeb.TestController do - use AtlasWeb, :controller - use PhoenixSwagger - - swagger_path :index do - get("/api/test") - summary("Health check") - description("Returns a simple OK message.") - produces("application/json") - response(200, "Success", Schema.ref(:HealthResponse)) - end - - def index(conn, _params) do - json(conn, %{message: "ok"}) - end - - def swagger_definitions do - %{ - HealthResponse: - swagger_schema do - title("HealthCheck") - description("Simple response to confirm service is alive") - - properties do - message(:string, "Confirmation message") - end - - example(%{message: "ok"}) - end - } - end -end diff --git a/lib/atlas_web/endpoint.ex b/lib/atlas_web/endpoint.ex index 49dbb5b..fb8245a 100644 --- a/lib/atlas_web/endpoint.ex +++ b/lib/atlas_web/endpoint.ex @@ -23,7 +23,7 @@ defmodule AtlasWeb.Endpoint do at: "/", from: :atlas, gzip: false, - only: ~w(css fonts images js favicon.ico robots.txt swagger.json) + only: AtlasWeb.static_paths() # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. diff --git a/priv/static/swagger.json b/priv/static/swagger.json index afbe819..d655980 100644 --- a/priv/static/swagger.json +++ b/priv/static/swagger.json @@ -4,45 +4,7 @@ "title": "Atlas" }, "host": "localhost:4000", - "definitions": { - "HealthResponse": { - "description": "Simple response to confirm service is alive", - "example": { - "message": "ok" - }, - "properties": { - "message": { - "description": "Confirmation message", - "type": "string" - } - }, - "title": "HealthCheck", - "type": "object" - } - }, - "paths": { - "/api/test": { - "get": { - "description": "Returns a simple OK message.", - "operationId": "AtlasWeb.TestController.index", - "parameters": [], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/HealthResponse" - } - } - }, - "summary": "Health check", - "tags": [ - "Test" - ] - } - } - }, + "definitions": {}, + "paths": {}, "swagger": "2.0" } \ No newline at end of file From 707fc79a7c5eafdd599666f920033709e7a2b9db Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Fri, 11 Jul 2025 13:40:13 +0100 Subject: [PATCH 05/10] fix: remove unused route --- lib/atlas_web/router.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/atlas_web/router.ex b/lib/atlas_web/router.ex index e738cdd..534e4bd 100644 --- a/lib/atlas_web/router.ex +++ b/lib/atlas_web/router.ex @@ -9,8 +9,6 @@ defmodule AtlasWeb.Router do scope "/api", AtlasWeb do pipe_through :api - - get "/test", TestController, :index end scope "/swagger" do From 85ade9e9798bbc918250d8ee6fede830f61580a1 Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Sat, 12 Jul 2025 13:57:19 +0100 Subject: [PATCH 06/10] feat: add some paths --- lib/atlas_web/controllers/auth_controller.ex | 46 +++++++++++++++ lib/atlas_web/router.ex | 8 +-- priv/static/swagger.json | 62 +++++++++++++++++++- 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/lib/atlas_web/controllers/auth_controller.ex b/lib/atlas_web/controllers/auth_controller.ex index ce29715..35372ba 100644 --- a/lib/atlas_web/controllers/auth_controller.ex +++ b/lib/atlas_web/controllers/auth_controller.ex @@ -3,6 +3,7 @@ defmodule AtlasWeb.AuthController do alias Atlas.Accounts alias Atlas.Accounts.{Guardian, User} + use PhoenixSwagger action_fallback AtlasWeb.FallbackController @@ -221,4 +222,49 @@ defmodule AtlasWeb.AuthController do agent: user_agent } end + + swagger_path :sign_in do + post("/v1/auth/sign_in") + summary("Sigin in a user") + description("Sign in a user. Returns an access token and a refresh token.") + produces("application/json") + tag ("Authentication") + operation_id("sign_in") + parameters do + email(:query, :string, "User email", required: true) + password(:query, :string, "User password", required: true) + end + response(200, "Successful sign in") + response(401, "Unauthorized") + end + + swagger_path :refresh_token do + post("/v1/auth/refresh") + summary("Refresh access token") + description("Refresh access token with a refresh token cookie.") + produces("application/json") + tag ("Authentication") + operation_id("refresh_token") + response(200, "Successful refresh") + response(401, "Unauthorized") + end + + swagger_path :forgot_password do + post("/v1/auth/forgot_password") + summary("Request password reset") + description("Sends a password request to the user") + produces("application/json") + tag ("Authentication") + operation_id("forgot_password") + response(200, "Request succesfully sent") + response(401, "Unauthorized") + end + + swagger_path :reset_password do + post("/v1/auth/reset_password") + summary("Reset password") + description("Sends a request to reset the password.") + end + + end diff --git a/lib/atlas_web/router.ex b/lib/atlas_web/router.ex index 7a25eb4..14aedcb 100644 --- a/lib/atlas_web/router.ex +++ b/lib/atlas_web/router.ex @@ -1,10 +1,8 @@ defmodule AtlasWeb.Router do use AtlasWeb, :router - alias PhoenixSwagger.Plug.Validate pipeline :api do plug :accepts, ["json"] - plug(Validate, validation_failed_status: 422) plug RemoteIp end @@ -12,9 +10,9 @@ defmodule AtlasWeb.Router do plug :accepts, ["json"] plug Guardian.Plug.Pipeline, - otp_app: :atlas, - error_handler: AtlasWeb.Plugs.AuthErrorHandler, - module: Atlas.Accounts.Guardian + otp_app: :atlas, + error_handler: AtlasWeb.Plugs.AuthErrorHandler, + module: Atlas.Accounts.Guardian plug Guardian.Plug.VerifyHeader, claims: %{typ: "access"} plug Guardian.Plug.EnsureAuthenticated diff --git a/priv/static/swagger.json b/priv/static/swagger.json index d655980..a54a6c8 100644 --- a/priv/static/swagger.json +++ b/priv/static/swagger.json @@ -5,6 +5,66 @@ }, "host": "localhost:4000", "definitions": {}, - "paths": {}, + "paths": { + "/v1/auth/refresh": { + "post": { + "description": "Refresh access token", + "operationId": "refresh_token", + "parameters": [], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Successful refresh" + }, + "401": { + "description": "Unauthorized" + } + }, + "summary": "Refresh access token", + "tags": [ + "Authentication" + ] + } + }, + "/v1/auth/sign_in": { + "post": { + "description": "Sign in a user. Returns an access token and a refresh token.", + "operationId": "sign_in", + "parameters": [ + { + "description": "User email", + "in": "query", + "name": "email", + "required": true, + "type": "string" + }, + { + "description": "User password", + "in": "query", + "name": "password", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Successful sign in" + }, + "401": { + "description": "Unauthorized" + } + }, + "summary": "Sigin in a user", + "tags": [ + "Authentication" + ] + } + } + }, "swagger": "2.0" } \ No newline at end of file From ede9adcf73d8e51994580366fed5197f7ab71b31 Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Sat, 12 Jul 2025 16:57:51 +0100 Subject: [PATCH 07/10] feat: all swagger paths --- lib/atlas_web/controllers/auth_controller.ex | 55 ++++++- priv/static/swagger.json | 150 ++++++++++++++++++- 2 files changed, 199 insertions(+), 6 deletions(-) diff --git a/lib/atlas_web/controllers/auth_controller.ex b/lib/atlas_web/controllers/auth_controller.ex index 35372ba..7dd2260 100644 --- a/lib/atlas_web/controllers/auth_controller.ex +++ b/lib/atlas_web/controllers/auth_controller.ex @@ -225,7 +225,7 @@ defmodule AtlasWeb.AuthController do swagger_path :sign_in do post("/v1/auth/sign_in") - summary("Sigin in a user") + summary("Sign in a user") description("Sign in a user. Returns an access token and a refresh token.") produces("application/json") tag ("Authentication") @@ -236,6 +236,7 @@ defmodule AtlasWeb.AuthController do end response(200, "Successful sign in") response(401, "Unauthorized") + response(500, "Failed to create user session") end swagger_path :refresh_token do @@ -252,19 +253,65 @@ defmodule AtlasWeb.AuthController do swagger_path :forgot_password do post("/v1/auth/forgot_password") summary("Request password reset") - description("Sends a password request to the user") + description("Sends password reset instructions to the user.") produces("application/json") tag ("Authentication") operation_id("forgot_password") - response(200, "Request succesfully sent") + parameters do + email(:query, :string, "User email", required: true) + end + response(204, "No content") response(401, "Unauthorized") end swagger_path :reset_password do post("/v1/auth/reset_password") summary("Reset password") - description("Sends a request to reset the password.") + description("Sends a request to reset user's password.") + produces("application/json") + tag("Authentication") + operation_id("reset_password") + parameters do + token(:query, :string, "Access token", required: true) + password(:query, :string, "New password", required: true) + password_confirmation(:query, :string, "New password confirmation", required: true) + end + response(200, "Password succesfully reset") + response(404, "Invalid or expired reset token") end + swagger_path :sign_out do + post("/v1/auth/sign_out") + summary("Sign out") + description("Signs out the user.") + produces("application/json") + tag("Authentication") + operation_id("sign_out") + response(200, "Signed out successfully") + response(401, "Unauthorized") + response(500, "Failed to sign out") + end + + swagger_path :me do + get("/v1/auth/me") + summary("User in the current session") + description("Returns the user in the current session.") + produces("application/json") + tag("Authentication") + operation_id("me") + response(200, "User returned succesfully") + response(401, "Unauthorized") + end + + swagger_path :sessions do + get("/v1/auth/sessions") + summary("User sessions") + description("Returns the user sessions.") + produces("application/json") + tag("Authentication") + operation_id("sessions") + response(200, "Sessions succesfully returned") + response(401, "Unauthorized") + end end diff --git a/priv/static/swagger.json b/priv/static/swagger.json index a54a6c8..975b96d 100644 --- a/priv/static/swagger.json +++ b/priv/static/swagger.json @@ -6,9 +6,61 @@ "host": "localhost:4000", "definitions": {}, "paths": { + "/v1/auth/forgot_password": { + "post": { + "description": "Sends password reset instructions to the user.", + "operationId": "forgot_password", + "parameters": [ + { + "description": "User email", + "in": "query", + "name": "email", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "204": { + "description": "No content" + }, + "401": { + "description": "Unauthorized" + } + }, + "summary": "Request password reset", + "tags": [ + "Authentication" + ] + } + }, + "/v1/auth/me": { + "get": { + "description": "Returns the user in the current session.", + "operationId": "me", + "parameters": [], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "User returned succesfully" + }, + "401": { + "description": "Unauthorized" + } + }, + "summary": "User in the current session", + "tags": [ + "Authentication" + ] + } + }, "/v1/auth/refresh": { "post": { - "description": "Refresh access token", + "description": "Refresh access token with a refresh token cookie.", "operationId": "refresh_token", "parameters": [], "produces": [ @@ -28,6 +80,72 @@ ] } }, + "/v1/auth/reset_password": { + "post": { + "description": "Sends a request to reset user's password.", + "operationId": "reset_password", + "parameters": [ + { + "description": "Access token", + "in": "query", + "name": "token", + "required": true, + "type": "string" + }, + { + "description": "New password", + "in": "query", + "name": "password", + "required": true, + "type": "string" + }, + { + "description": "New password confirmation", + "in": "query", + "name": "password_confirmation", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Password succesfully reset" + }, + "404": { + "description": "Invalid or expired reset token" + } + }, + "summary": "Reset password", + "tags": [ + "Authentication" + ] + } + }, + "/v1/auth/sessions": { + "get": { + "description": "Returns the user sessions.", + "operationId": "sessions", + "parameters": [], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Sessions succesfully returned" + }, + "401": { + "description": "Unauthorized" + } + }, + "summary": "User sessions", + "tags": [ + "Authentication" + ] + } + }, "/v1/auth/sign_in": { "post": { "description": "Sign in a user. Returns an access token and a refresh token.", @@ -57,9 +175,37 @@ }, "401": { "description": "Unauthorized" + }, + "500": { + "description": "Failed to create user session" + } + }, + "summary": "Sign in a user", + "tags": [ + "Authentication" + ] + } + }, + "/v1/auth/sign_out": { + "post": { + "description": "Signs out the user.", + "operationId": "sign_out", + "parameters": [], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Signed out successfully" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Failed to sign out" } }, - "summary": "Sigin in a user", + "summary": "Sign out", "tags": [ "Authentication" ] From 0b5dcdf5138a089c31346e11c68d07caccab1b5f Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Sat, 12 Jul 2025 17:55:32 +0100 Subject: [PATCH 08/10] feat: add bearer token auth --- lib/atlas_web/controllers/auth_controller.ex | 11 ++++--- lib/atlas_web/router.ex | 10 ++++++ priv/static/swagger.json | 34 ++++++++++++++++---- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/lib/atlas_web/controllers/auth_controller.ex b/lib/atlas_web/controllers/auth_controller.ex index 7dd2260..a69ce0a 100644 --- a/lib/atlas_web/controllers/auth_controller.ex +++ b/lib/atlas_web/controllers/auth_controller.ex @@ -226,7 +226,7 @@ defmodule AtlasWeb.AuthController do swagger_path :sign_in do post("/v1/auth/sign_in") summary("Sign in a user") - description("Sign in a user. Returns an access token and a refresh token.") + description("Sign in a user. Returns an access token.") produces("application/json") tag ("Authentication") operation_id("sign_in") @@ -253,7 +253,7 @@ defmodule AtlasWeb.AuthController do swagger_path :forgot_password do post("/v1/auth/forgot_password") summary("Request password reset") - description("Sends password reset instructions to the user.") + description("Sends password reset instructions to the user via email.") produces("application/json") tag ("Authentication") operation_id("forgot_password") @@ -287,9 +287,10 @@ defmodule AtlasWeb.AuthController do produces("application/json") tag("Authentication") operation_id("sign_out") - response(200, "Signed out successfully") + response(204, "No content - Signed out successfully") response(401, "Unauthorized") response(500, "Failed to sign out") + security [%{Bearer: []}] end swagger_path :me do @@ -301,17 +302,19 @@ defmodule AtlasWeb.AuthController do operation_id("me") response(200, "User returned succesfully") response(401, "Unauthorized") + security [%{Bearer: []}] end swagger_path :sessions do get("/v1/auth/sessions") summary("User sessions") - description("Returns the user sessions.") + description("Returns all the user sessions.") produces("application/json") tag("Authentication") operation_id("sessions") response(200, "Sessions succesfully returned") response(401, "Unauthorized") + security [%{Bearer: []}] end end diff --git a/lib/atlas_web/router.ex b/lib/atlas_web/router.ex index 14aedcb..87dc89a 100644 --- a/lib/atlas_web/router.ex +++ b/lib/atlas_web/router.ex @@ -63,11 +63,21 @@ defmodule AtlasWeb.Router do end end + + # Usage for bearer token authorization: "Bearer " + def swagger_info do %{ info: %{ version: "0.1.0", title: "Atlas" + }, + securityDefinitions: %{ + Bearer: %{ + type: "apiKey", + name: "Authorization", + in: "header" + } } } end diff --git a/priv/static/swagger.json b/priv/static/swagger.json index 975b96d..156154a 100644 --- a/priv/static/swagger.json +++ b/priv/static/swagger.json @@ -8,7 +8,7 @@ "paths": { "/v1/auth/forgot_password": { "post": { - "description": "Sends password reset instructions to the user.", + "description": "Sends password reset instructions to the user via email.", "operationId": "forgot_password", "parameters": [ { @@ -52,6 +52,11 @@ "description": "Unauthorized" } }, + "security": [ + { + "Bearer": [] + } + ], "summary": "User in the current session", "tags": [ "Authentication" @@ -126,7 +131,7 @@ }, "/v1/auth/sessions": { "get": { - "description": "Returns the user sessions.", + "description": "Returns all the user sessions.", "operationId": "sessions", "parameters": [], "produces": [ @@ -140,6 +145,11 @@ "description": "Unauthorized" } }, + "security": [ + { + "Bearer": [] + } + ], "summary": "User sessions", "tags": [ "Authentication" @@ -148,7 +158,7 @@ }, "/v1/auth/sign_in": { "post": { - "description": "Sign in a user. Returns an access token and a refresh token.", + "description": "Sign in a user. Returns an access token.", "operationId": "sign_in", "parameters": [ { @@ -195,8 +205,8 @@ "application/json" ], "responses": { - "200": { - "description": "Signed out successfully" + "204": { + "description": "No content - Signed out successfully" }, "401": { "description": "Unauthorized" @@ -205,6 +215,11 @@ "description": "Failed to sign out" } }, + "security": [ + { + "Bearer": [] + } + ], "summary": "Sign out", "tags": [ "Authentication" @@ -212,5 +227,12 @@ } } }, - "swagger": "2.0" + "swagger": "2.0", + "securityDefinitions": { + "Bearer": { + "in": "header", + "name": "Authorization", + "type": "apiKey" + } + } } \ No newline at end of file From 7da387babdc0d4103598b378b19fa1825059750b Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Sat, 12 Jul 2025 17:56:03 +0100 Subject: [PATCH 09/10] format --- lib/atlas_web/controllers/auth_controller.ex | 19 ++++++++++++------- lib/atlas_web/router.ex | 7 +++---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/atlas_web/controllers/auth_controller.ex b/lib/atlas_web/controllers/auth_controller.ex index a69ce0a..0f6ac6f 100644 --- a/lib/atlas_web/controllers/auth_controller.ex +++ b/lib/atlas_web/controllers/auth_controller.ex @@ -228,12 +228,14 @@ defmodule AtlasWeb.AuthController do summary("Sign in a user") description("Sign in a user. Returns an access token.") produces("application/json") - tag ("Authentication") + tag("Authentication") operation_id("sign_in") + parameters do email(:query, :string, "User email", required: true) password(:query, :string, "User password", required: true) end + response(200, "Successful sign in") response(401, "Unauthorized") response(500, "Failed to create user session") @@ -244,7 +246,7 @@ defmodule AtlasWeb.AuthController do summary("Refresh access token") description("Refresh access token with a refresh token cookie.") produces("application/json") - tag ("Authentication") + tag("Authentication") operation_id("refresh_token") response(200, "Successful refresh") response(401, "Unauthorized") @@ -255,11 +257,13 @@ defmodule AtlasWeb.AuthController do summary("Request password reset") description("Sends password reset instructions to the user via email.") produces("application/json") - tag ("Authentication") + tag("Authentication") operation_id("forgot_password") + parameters do email(:query, :string, "User email", required: true) end + response(204, "No content") response(401, "Unauthorized") end @@ -271,11 +275,13 @@ defmodule AtlasWeb.AuthController do produces("application/json") tag("Authentication") operation_id("reset_password") + parameters do token(:query, :string, "Access token", required: true) password(:query, :string, "New password", required: true) password_confirmation(:query, :string, "New password confirmation", required: true) end + response(200, "Password succesfully reset") response(404, "Invalid or expired reset token") end @@ -290,7 +296,7 @@ defmodule AtlasWeb.AuthController do response(204, "No content - Signed out successfully") response(401, "Unauthorized") response(500, "Failed to sign out") - security [%{Bearer: []}] + security([%{Bearer: []}]) end swagger_path :me do @@ -302,7 +308,7 @@ defmodule AtlasWeb.AuthController do operation_id("me") response(200, "User returned succesfully") response(401, "Unauthorized") - security [%{Bearer: []}] + security([%{Bearer: []}]) end swagger_path :sessions do @@ -314,7 +320,6 @@ defmodule AtlasWeb.AuthController do operation_id("sessions") response(200, "Sessions succesfully returned") response(401, "Unauthorized") - security [%{Bearer: []}] + security([%{Bearer: []}]) end - end diff --git a/lib/atlas_web/router.ex b/lib/atlas_web/router.ex index 87dc89a..9822a79 100644 --- a/lib/atlas_web/router.ex +++ b/lib/atlas_web/router.ex @@ -10,9 +10,9 @@ defmodule AtlasWeb.Router do plug :accepts, ["json"] plug Guardian.Plug.Pipeline, - otp_app: :atlas, - error_handler: AtlasWeb.Plugs.AuthErrorHandler, - module: Atlas.Accounts.Guardian + otp_app: :atlas, + error_handler: AtlasWeb.Plugs.AuthErrorHandler, + module: Atlas.Accounts.Guardian plug Guardian.Plug.VerifyHeader, claims: %{typ: "access"} plug Guardian.Plug.EnsureAuthenticated @@ -63,7 +63,6 @@ defmodule AtlasWeb.Router do end end - # Usage for bearer token authorization: "Bearer " def swagger_info do From d2889992a174c275bf2c0a5264d5720f208e85f0 Mon Sep 17 00:00:00 2001 From: RicoPleasure Date: Tue, 15 Jul 2025 21:23:02 +0100 Subject: [PATCH 10/10] feat: add schemas for responses --- lib/atlas_web/controllers/auth_controller.ex | 345 +++++++++++++------ priv/static/swagger.json | 313 ++++++++++++++++- 2 files changed, 542 insertions(+), 116 deletions(-) diff --git a/lib/atlas_web/controllers/auth_controller.ex b/lib/atlas_web/controllers/auth_controller.ex index 0f6ac6f..7e40114 100644 --- a/lib/atlas_web/controllers/auth_controller.ex +++ b/lib/atlas_web/controllers/auth_controller.ex @@ -1,15 +1,33 @@ defmodule AtlasWeb.AuthController do use AtlasWeb, :controller + use PhoenixSwagger alias Atlas.Accounts alias Atlas.Accounts.{Guardian, User} - use PhoenixSwagger action_fallback AtlasWeb.FallbackController @refresh_token_days 7 @audience "astra" + swagger_path :sign_in do + post("/v1/auth/sign_in") + summary("Sign in a user") + description("Sign in a user. Returns an access token.") + produces("application/json") + tag("Authentication") + operation_id("sign_in") + + parameters do + email(:query, :string, "User email", required: true) + password(:query, :string, "User password", required: true) + end + + response(200, "Successful sign in", Schema.ref(:SignInResponse)) + response(401, "Unauthorized", Schema.ref(:UnauthorizedResponse)) + response(500, "Failed to create user session", Schema.ref(:ErrorResponse)) + end + def sign_in(conn, %{"email" => email, "password" => password}) do case Accounts.get_user_by_email_and_password(email, password) do %User{} = user -> @@ -49,6 +67,18 @@ defmodule AtlasWeb.AuthController do end end + swagger_path :me do + get("/v1/auth/me") + summary("User in the current session") + description("Returns the user in the current session.") + produces("application/json") + tag("Authentication") + operation_id("me") + response(200, "User returned succesfully", Schema.ref(:User)) + response(401, "Unauthorized", Schema.ref(:UnauthorizedResponse)) + security([%{Bearer: []}]) + end + def me(conn, _params) do {user, _session} = Guardian.Plug.current_resource(conn) @@ -63,6 +93,17 @@ defmodule AtlasWeb.AuthController do end end + swagger_path :refresh_token do + post("/v1/auth/refresh") + summary("Refresh access token") + description("Refresh access token with a refresh token cookie.") + produces("application/json") + tag("Authentication") + operation_id("refresh_token") + response(200, "Successful refresh", Schema.ref(:SuccessfulRefreshResponse)) + response(401, "Unauthorized", Schema.ref(:UnauthorizedResponse)) + end + def refresh_token(conn, _params) do case fetch_refresh_token_cookie(conn) do {:ok, old_refresh_token} -> @@ -92,6 +133,19 @@ defmodule AtlasWeb.AuthController do end end + swagger_path :sign_out do + post("/v1/auth/sign_out") + summary("Sign out") + description("Signs out the user.") + produces("application/json") + tag("Authentication") + operation_id("sign_out") + response(204, "No content - Signed out successfully", Schema.ref(:SignOutResponse)) + response(401, "Unauthorized", Schema.ref(:UnauthorizedResponse)) + response(500, "Failed to sign out", Schema.ref(:ErrorResponse)) + security([%{Bearer: []}]) + end + def sign_out(conn, _params) do {_user, session} = Guardian.Plug.current_resource(conn) @@ -115,6 +169,19 @@ defmodule AtlasWeb.AuthController do end end + swagger_path :sessions do + get("/v1/auth/sessions") + summary("User sessions") + description("Returns all the user sessions.") + produces("application/json") + tag("Authentication") + operation_id("sessions") + response(200, "Sessions succesfully returned", Schema.ref(:UserSessionsResponse)) + response(401, "Unauthorized", Schema.ref(:UnauthorizedResponse)) + + security([%{Bearer: []}]) + end + def sessions(conn, _params) do {user, _session} = Guardian.Plug.current_resource(conn) @@ -131,6 +198,22 @@ defmodule AtlasWeb.AuthController do end end + swagger_path :forgot_password do + post("/v1/auth/forgot_password") + summary("Request password reset") + description("Sends password reset instructions to the user via email.") + produces("application/json") + tag("Authentication") + operation_id("forgot_password") + + parameters do + email(:query, :string, "User email", required: true) + end + + response(204, "No content", Schema.ref(:NoContentResponse)) + response(401, "Unauthorized", Schema.ref(:UnauthorizedResponse)) + end + def forgot_password(conn, %{"email" => email}) do if user = Accounts.get_user_by_email(email) do Accounts.deliver_user_reset_password_instructions(user, &"/auth/forgot_password/#{&1}") @@ -141,6 +224,24 @@ defmodule AtlasWeb.AuthController do |> send_resp(:no_content, "") end + swagger_path :reset_password do + post("/v1/auth/reset_password") + summary("Reset password") + description("Sends a request to reset user's password.") + produces("application/json") + tag("Authentication") + operation_id("reset_password") + + parameters do + token(:query, :string, "Access token", required: true) + password(:query, :string, "New password", required: true) + password_confirmation(:query, :string, "New password confirmation", required: true) + end + + response(200, "Password succesfully reset", Schema.ref(:ResetPasswordResponse)) + response(404, "Invalid or expired reset token", Schema.ref(:ErrorResponse)) + end + def reset_password(conn, %{ "token" => token, "password" => new_password, @@ -223,103 +324,149 @@ defmodule AtlasWeb.AuthController do } end - swagger_path :sign_in do - post("/v1/auth/sign_in") - summary("Sign in a user") - description("Sign in a user. Returns an access token.") - produces("application/json") - tag("Authentication") - operation_id("sign_in") - - parameters do - email(:query, :string, "User email", required: true) - password(:query, :string, "User password", required: true) - end - - response(200, "Successful sign in") - response(401, "Unauthorized") - response(500, "Failed to create user session") - end - - swagger_path :refresh_token do - post("/v1/auth/refresh") - summary("Refresh access token") - description("Refresh access token with a refresh token cookie.") - produces("application/json") - tag("Authentication") - operation_id("refresh_token") - response(200, "Successful refresh") - response(401, "Unauthorized") - end - - swagger_path :forgot_password do - post("/v1/auth/forgot_password") - summary("Request password reset") - description("Sends password reset instructions to the user via email.") - produces("application/json") - tag("Authentication") - operation_id("forgot_password") - - parameters do - email(:query, :string, "User email", required: true) - end - - response(204, "No content") - response(401, "Unauthorized") - end - - swagger_path :reset_password do - post("/v1/auth/reset_password") - summary("Reset password") - description("Sends a request to reset user's password.") - produces("application/json") - tag("Authentication") - operation_id("reset_password") - - parameters do - token(:query, :string, "Access token", required: true) - password(:query, :string, "New password", required: true) - password_confirmation(:query, :string, "New password confirmation", required: true) - end - - response(200, "Password succesfully reset") - response(404, "Invalid or expired reset token") - end - - swagger_path :sign_out do - post("/v1/auth/sign_out") - summary("Sign out") - description("Signs out the user.") - produces("application/json") - tag("Authentication") - operation_id("sign_out") - response(204, "No content - Signed out successfully") - response(401, "Unauthorized") - response(500, "Failed to sign out") - security([%{Bearer: []}]) - end - - swagger_path :me do - get("/v1/auth/me") - summary("User in the current session") - description("Returns the user in the current session.") - produces("application/json") - tag("Authentication") - operation_id("me") - response(200, "User returned succesfully") - response(401, "Unauthorized") - security([%{Bearer: []}]) - end - - swagger_path :sessions do - get("/v1/auth/sessions") - summary("User sessions") - description("Returns all the user sessions.") - produces("application/json") - tag("Authentication") - operation_id("sessions") - response(200, "Sessions succesfully returned") - response(401, "Unauthorized") - security([%{Bearer: []}]) + def swagger_definitions do + %{ + SignInResponse: + swagger_schema do + title("SignInResponse") + description("Response schema for successful sign in") + + properties do + session_id(:integer, "User session ID", required: true) + access_token(:string, "Access token", required: true) + end + + example(%{ + session_id: "e1387cae-ac1d-4aeb-8e13-ff1b3dd15ca4", + access_token: + "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhc3RyYSIsImV4cCI6MTc1MjYxMTYwOCwiaWF0IjoxNzUyNjEwNzA4LCJpc3MiOiJhdGxhcyIsImp0aSI6IjYyNjM2ZWFmLTVmZGQtNGU2My05ZmI1LWQyZjYwNmQzOGUzNSIsIm5iZiI6MTc1MjYxMDcwNywic3ViIjoiZTEzODdjYWUtYWMxZC00YWViLThlMTMtZmYxYjNkZDE1Y2E0IiwidHlwIjoiYWNjZXNzIn0.bAF6nLXPlHH80jhueetNyC5jZQ4rXXO1MO63izQ-7x98flalF6IGxc8v3HGLSRfF7s3cXYVOteeSvUUUqbx60A" + }) + end, + ErrorResponse: + swagger_schema do + title("ErrorResponse") + description("Error response schema") + + properties do + error(:string, "Error message", required: true) + end + end, + UnauthorizedResponse: + swagger_schema do + title("UnauthorizedResponse") + description("Unauthorized response schema") + + properties do + error(:string, "Unauthorized error message", required: true) + end + end, + User: + swagger_schema do + title("User") + description("User schema") + + properties do + id(:integer, "User ID", required: true) + name(:string, "User name", required: false) + inserted_at(:string, "Creation timestamp", format: "date-time", required: true) + email(:string, "User email", required: true) + updated_at(:string, "Last update timestamp", format: "date-time", required: true) + end + + example(%{ + user: %{ + id: "d18472e7-5251-4027-884f-58b8a3a6abe5", + name: "Leonardo Carvalho", + inserted_at: "2025-07-15T18:10:27Z", + email: "a114437@alunos.uminho.pt", + updated_at: "2025-07-15T18:10:27Z" + } + }) + end, + SuccessfulRefreshResponse: + swagger_schema do + title("SuccessfulRefreshResponse") + description("Response schema for successful token refresh") + + properties do + access_token(:string, "New access token", required: true) + end + + example(%{ + access_token: + "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhc3RyYSIsImV4cCI6MTc1MjYxMTY5NCwiaWF0IjoxNzUyNjEwNzk0LCJpc3MiOiJhdGxhcyIsImp0aSI6ImYwOTg4MDMzLTlhNDktNGUzZC04M2U5LWE3NDVkZDkwYmY5ZiIsIm5iZiI6MTc1MjYxMDc5Mywic3ViIjoiZTEzODdjYWUtYWMxZC00YWViLThlMTMtZmYxYjNkZDE1Y2E0IiwidHlwIjoiYWNjZXNzIn0.ztcw5nZ3cdI1v5iTU0ZHyx-xZgWukxeFpuMulhvar7iRfSubBztlggVxpVM8bD-ulmujuX1i3-ksbfSdpNYMTQ" + }) + end, + SignOutResponse: + swagger_schema do + title("SignOutResponse") + description("Response schema for successful sign out") + + properties do + message(:string, "Message indicating successful sign out", required: true) + end + + example(%{message: "Signed out successfully"}) + end, + UserSessionsResponse: + swagger_schema do + title("UserSessionsResponse") + description("Response schema for a list of user sessions") + + properties do + sessions(Schema.array(:UserSession), "List of user sessions", required: true) + end + + example(%{ + sessions: [ + %{ + id: "8fd2bef3-f1eb-4bf2-aade-f3ae80e0563d", + ip: "127.0.0.1", + user_agent: + "Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0", + user_browser: "Firefox", + user_os: "Linux", + first_seen: "2025-07-15T20:13:41Z" + } + ] + }) + end, + UserSession: + swagger_schema do + title("User Session") + description("User session schema") + + properties do + id(:integer, "Session ID", required: true) + ip(:string, "IP address of the session", required: true) + user_agent(:string, "User agent string", required: true) + user_browser(:string, "Browser of the user agent", required: true) + user_os(:string, "Operating system of the user agent", required: true) + first_seen(:string, "First seen timestamp", format: "date-time", required: true) + end + end, + NoContentResponse: + swagger_schema do + title("NoContentResponse") + description("Response schema for no content") + + properties do + message(:string, "Message indicating no content", required: true) + end + + example(%{}) + end, + ResetPasswordResponse: + swagger_schema do + title("ResetPasswordResponse") + description("Response schema for successful password reset") + + properties do + message(:string, "Message indicating successful password reset", required: true) + end + + example(%{message: "Password reset successfully"}) + end + } end end diff --git a/priv/static/swagger.json b/priv/static/swagger.json index 156154a..0f6e0f9 100644 --- a/priv/static/swagger.json +++ b/priv/static/swagger.json @@ -4,7 +4,238 @@ "title": "Atlas" }, "host": "localhost:4000", - "definitions": {}, + "definitions": { + "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" + }, + "updated_at": { + "description": "Last update timestamp", + "format": "date-time", + "type": "string" + } + }, + "required": [ + "updated_at", + "email", + "inserted_at", + "id" + ], + "title": "User", + "type": "object" + }, + "SignInResponse": { + "description": "Response schema for successful sign in", + "example": { + "access_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhc3RyYSIsImV4cCI6MTc1MjYxMTYwOCwiaWF0IjoxNzUyNjEwNzA4LCJpc3MiOiJhdGxhcyIsImp0aSI6IjYyNjM2ZWFmLTVmZGQtNGU2My05ZmI1LWQyZjYwNmQzOGUzNSIsIm5iZiI6MTc1MjYxMDcwNywic3ViIjoiZTEzODdjYWUtYWMxZC00YWViLThlMTMtZmYxYjNkZDE1Y2E0IiwidHlwIjoiYWNjZXNzIn0.bAF6nLXPlHH80jhueetNyC5jZQ4rXXO1MO63izQ-7x98flalF6IGxc8v3HGLSRfF7s3cXYVOteeSvUUUqbx60A", + "session_id": "e1387cae-ac1d-4aeb-8e13-ff1b3dd15ca4" + }, + "properties": { + "access_token": { + "description": "Access token", + "type": "string" + }, + "session_id": { + "description": "User session ID", + "type": "integer" + } + }, + "required": [ + "access_token", + "session_id" + ], + "title": "SignInResponse", + "type": "object" + }, + "UnauthorizedResponse": { + "description": "Unauthorized response schema", + "properties": { + "error": { + "description": "Unauthorized error message", + "type": "string" + } + }, + "required": [ + "error" + ], + "title": "UnauthorizedResponse", + "type": "object" + }, + "ErrorResponse": { + "description": "Error response schema", + "properties": { + "error": { + "description": "Error message", + "type": "string" + } + }, + "required": [ + "error" + ], + "title": "ErrorResponse", + "type": "object" + }, + "SuccessfulRefreshResponse": { + "description": "Response schema for successful token refresh", + "example": { + "access_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhc3RyYSIsImV4cCI6MTc1MjYxMTY5NCwiaWF0IjoxNzUyNjEwNzk0LCJpc3MiOiJhdGxhcyIsImp0aSI6ImYwOTg4MDMzLTlhNDktNGUzZC04M2U5LWE3NDVkZDkwYmY5ZiIsIm5iZiI6MTc1MjYxMDc5Mywic3ViIjoiZTEzODdjYWUtYWMxZC00YWViLThlMTMtZmYxYjNkZDE1Y2E0IiwidHlwIjoiYWNjZXNzIn0.ztcw5nZ3cdI1v5iTU0ZHyx-xZgWukxeFpuMulhvar7iRfSubBztlggVxpVM8bD-ulmujuX1i3-ksbfSdpNYMTQ" + }, + "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", + "type": "string" + } + }, + "required": [ + "message" + ], + "title": "SignOutResponse", + "type": "object" + }, + "UserSessionsResponse": { + "description": "Response schema for a list of user sessions", + "example": { + "sessions": [ + { + "first_seen": "2025-07-15T20:13:41Z", + "id": "8fd2bef3-f1eb-4bf2-aade-f3ae80e0563d", + "ip": "127.0.0.1", + "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0", + "user_browser": "Firefox", + "user_os": "Linux" + } + ] + }, + "properties": { + "sessions": { + "description": "List of user sessions", + "items": { + "$ref": "#/definitions/UserSession" + }, + "type": "array" + } + }, + "required": [ + "sessions" + ], + "title": "UserSessionsResponse", + "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" + }, + "UserSession": { + "description": "User session schema", + "properties": { + "first_seen": { + "description": "First seen timestamp", + "format": "date-time", + "type": "string" + }, + "id": { + "description": "Session ID", + "type": "integer" + }, + "ip": { + "description": "IP address of the session", + "type": "string" + }, + "user_agent": { + "description": "User agent string", + "type": "string" + }, + "user_browser": { + "description": "Browser of the user agent", + "type": "string" + }, + "user_os": { + "description": "Operating system of the user agent", + "type": "string" + } + }, + "required": [ + "first_seen", + "user_os", + "user_browser", + "user_agent", + "ip", + "id" + ], + "title": "User Session", + "type": "object" + } + }, "paths": { "/v1/auth/forgot_password": { "post": { @@ -24,10 +255,16 @@ ], "responses": { "204": { - "description": "No content" + "description": "No content", + "schema": { + "$ref": "#/definitions/NoContentResponse" + } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/UnauthorizedResponse" + } } }, "summary": "Request password reset", @@ -46,10 +283,16 @@ ], "responses": { "200": { - "description": "User returned succesfully" + "description": "User returned succesfully", + "schema": { + "$ref": "#/definitions/User" + } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/UnauthorizedResponse" + } } }, "security": [ @@ -73,10 +316,16 @@ ], "responses": { "200": { - "description": "Successful refresh" + "description": "Successful refresh", + "schema": { + "$ref": "#/definitions/SuccessfulRefreshResponse" + } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/UnauthorizedResponse" + } } }, "summary": "Refresh access token", @@ -117,10 +366,16 @@ ], "responses": { "200": { - "description": "Password succesfully reset" + "description": "Password succesfully reset", + "schema": { + "$ref": "#/definitions/ResetPasswordResponse" + } }, "404": { - "description": "Invalid or expired reset token" + "description": "Invalid or expired reset token", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } } }, "summary": "Reset password", @@ -139,10 +394,16 @@ ], "responses": { "200": { - "description": "Sessions succesfully returned" + "description": "Sessions succesfully returned", + "schema": { + "$ref": "#/definitions/UserSessionsResponse" + } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/UnauthorizedResponse" + } } }, "security": [ @@ -181,13 +442,22 @@ ], "responses": { "200": { - "description": "Successful sign in" + "description": "Successful sign in", + "schema": { + "$ref": "#/definitions/SignInResponse" + } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/UnauthorizedResponse" + } }, "500": { - "description": "Failed to create user session" + "description": "Failed to create user session", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } } }, "summary": "Sign in a user", @@ -206,13 +476,22 @@ ], "responses": { "204": { - "description": "No content - Signed out successfully" + "description": "No content - Signed out successfully", + "schema": { + "$ref": "#/definitions/SignOutResponse" + } }, "401": { - "description": "Unauthorized" + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/UnauthorizedResponse" + } }, "500": { - "description": "Failed to sign out" + "description": "Failed to sign out", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } } }, "security": [