Skip to content

feat: swagger & swagger-ui #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
2 changes: 1 addition & 1 deletion lib/atlas_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
248 changes: 248 additions & 0 deletions lib/atlas_web/controllers/auth_controller.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule AtlasWeb.AuthController do
use AtlasWeb, :controller
use PhoenixSwagger

alias Atlas.Accounts
alias Atlas.Accounts.{Guardian, User}
Expand All @@ -9,6 +10,24 @@ defmodule AtlasWeb.AuthController do
@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 ->
Expand Down Expand Up @@ -48,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)

Expand All @@ -62,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} ->
Expand Down Expand Up @@ -91,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)

Expand All @@ -114,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)

Expand All @@ -130,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}")
Expand All @@ -140,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,
Expand Down Expand Up @@ -221,4 +323,150 @@ defmodule AtlasWeb.AuthController do
agent: user_agent
}
end

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
22 changes: 22 additions & 0 deletions lib/atlas_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ defmodule AtlasWeb.Router do
end
end

scope "/swagger" do
forward("/", PhoenixSwagger.Plug.SwaggerUI, otp_app: :atlas, swagger_file: "swagger.json")
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
Expand All @@ -58,4 +62,22 @@ defmodule AtlasWeb.Router do
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end

# Usage for bearer token authorization: "Bearer <token>"

def swagger_info do
%{
info: %{
version: "0.1.0",
title: "Atlas"
},
securityDefinitions: %{
Bearer: %{
type: "apiKey",
name: "Authorization",
in: "header"
}
}
}
end
end
5 changes: 4 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ defmodule Atlas.MixProject do

# utilities
{:remote_ip, "~> 1.2"},
{:ua_parser, "~> 1.8"}
{:ua_parser, "~> 1.8"},

# swagger
{:phoenix_swagger, "~> 0.8", only: [:dev, :test], runtime: false}
]
end

Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,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"},
Expand Down
Loading