Skip to content
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
6 changes: 6 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]

# Configures Waffle
config :waffle,
storage: Waffle.Storage.Local,
storage_dir_prefix: "priv",
asset_host: {:system, "ASSET_HOST"}

# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

Expand Down
20 changes: 20 additions & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,25 @@ config :swoosh, local: false
# Do not print debug messages in production
config :logger, level: :info

# Configures Waffle
config :waffle,
storage: Waffle.Storage.S3,
bucket: {:system, "AWS_S3_BUCKET"},
asset_host: {:system, "ASSET_HOST"}

# Configure ExAws
config :ex_aws,
json_codec: Jason,
access_key_id: {:system, "AWS_ACCESS_KEY_ID"},
secret_access_key: {:system, "AWS_SECRET_ACCESS_KEY"},
region: {:system, "AWS_REGION"},
s3: [
scheme: "https://",
host: {:system, "ASSET_HOST"},
region: {:system, "AWS_REGION"},
access_key_id: {:system, "AWS_ACCESS_KEY_ID"},
secret_access_key: {:system, "AWS_SECRET_ACCESS_KEY"}
]

# Runtime production configuration, including reading
# of environment variables, is done on config/runtime.exs.
36 changes: 36 additions & 0 deletions lib/atlas/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Atlas.Accounts do

alias Atlas.Accounts.{User, UserNotifier, UserPreference, UserSession, UserToken}
alias Atlas.University.Student
alias Atlas.Uploaders.UserAvatar

## Database getters

Expand Down Expand Up @@ -599,4 +600,39 @@ defmodule Atlas.Accounts do
conflict_target: :user_id
)
end

@doc """
Updates an user's avatar.
"""

def update_user_avatar(%User{} = user, attrs) do
user
|> User.avatar_changeset(attrs)
|> Repo.update()
end

@doc """
Gets the avatar url.

## Examples
... (not tested yet)
"""

def get_user_avatar_url(%User{} = user) do
UserAvatar.url({user.avatar, user})
end

@doc """
Deletes a user's avatar.
"""

def remove_user_avatar(%User{} = user) do
if user.avatar do
UserAvatar.delete({user.avatar, user})
end

user
|> User.avatar_changeset(%{avatar: nil})
|> Repo.update()
end
end
10 changes: 10 additions & 0 deletions lib/atlas/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule Atlas.Accounts.User do
Application user schema and changesets.
"""
use Atlas.Schema
use Waffle.Ecto.Schema

alias Atlas.University

Expand All @@ -14,6 +15,7 @@ defmodule Atlas.Accounts.User do
field :current_password, :string, virtual: true, redact: true
field :confirmed_at, :utc_datetime
field :type, Ecto.Enum, values: [:student, :admin, :professor]
field :avatar, Atlas.Uploaders.UserAvatar.Type

has_one :student, University.Student, on_delete: :delete_all

Expand Down Expand Up @@ -167,4 +169,12 @@ defmodule Atlas.Accounts.User do
add_error(changeset, :current_password, "is not valid")
end
end

@doc """
A user changeset for updating avatar
"""
def avatar_changeset(user, attrs) do
user
|> cast_attachments(attrs, [:avatar])
end
end
15 changes: 15 additions & 0 deletions lib/atlas/uploader.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Atlas.Uploader do
@moduledoc """
Base uploader module.
"""
defmacro __using__(_) do
quote do
use Waffle.Definition
use Waffle.Ecto.Definition

def s3_object_headers(_version, {file, _scope}) do
[content_type: MIME.from_path(file.file_name)]
end
end
end
end
35 changes: 35 additions & 0 deletions lib/atlas/uploaders/user_avatar.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule Atlas.Uploaders.UserAvatar do
@moduledoc """
User Avatar image uploader.
"""
use Atlas.Uploader

@versions [:original]
@extension_whitelist ~w(.jpg .jpeg .png)

def validate({file, _}) do
file_extension = file.file_name |> Path.extname() |> String.downcase()

case Enum.member?(extension_whitelist(), file_extension) do
true -> :ok
false -> {:error, "Invalid file type"}
end
end

def storage_dir(_version, {_file, %{id: user_id}}) do
"uploads/user/avatars/#{user_id}"
end

def filename(version, _) do
version
end

def extension_whitelist do
@extension_whitelist
end

# Provide a default URL if there hasn't been a file uploaded
# def default_url(version, scope) do
# "/images/avatars/default_#{version}.png"
# end
end
194 changes: 194 additions & 0 deletions lib/atlas_web/controllers/user_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
defmodule AtlasWeb.UserController do
use AtlasWeb, :controller
use PhoenixSwagger

alias Atlas.Accounts

def upload_avatar(conn, %{"id" => user_id, "avatar" => upload}) do
user_id
|> get_user()
|> update_user_avatar(upload)
|> send_upload_response(conn)
end

def upload_avatar(conn, %{"id" => _user_id}) do
conn
|> put_status(:unprocessable_entity)
|> json(%{status: "error", message: "No avatar file provided"})
end

defp send_upload_response({:ok, user}, conn) do
conn
|> put_status(:ok)
|> json(%{
status: "success",
message: "Avatar uploaded successfully",
data: %{avatar_url: Accounts.get_user_avatar_url(user), user_id: user.id}
})
end

defp send_upload_response({:error, reason}, conn) do
{status, message} =
case reason do
:not_found -> {:not_found, "User not found"}
%Ecto.Changeset{} -> {:unprocessable_entity, "Avatar validation failed"}
_ -> {:unprocessable_entity, "Upload failed"}
end

conn |> put_status(status) |> json(%{status: "error", message: message})
end

swagger_path :upload_avatar do
post("/v1/users/{id}/avatar")
summary("Upload user avatar")
description("Upload an avatar image for a specific user")
produces("application/json")
tag("Uploaders")
consumes("multipart/form-data")
security([%{Bearer: []}])

parameters do
id(:path, :string, "User ID", required: true)
avatar(:formData, :file, "Avatar image file", required: true)
end

response(200, "Success", Schema.ref(:AvatarUploadSuccess))
response(422, "Validation error", Schema.ref(:ErrorResponse))
end

defp get_user(user_id) do
case Accounts.get_user(user_id) do
%Atlas.Accounts.User{} = user -> {:ok, user}
nil -> {:error, :not_found}
end
end

defp update_user_avatar({:ok, user}, upload) do
Accounts.update_user_avatar(user, %{avatar: upload})
end

defp update_user_avatar(error, _upload), do: error

def delete_avatar(conn, %{"id" => user_id}) do
user_id
|> get_user()
|> remove_user_avatar()
|> send_delete_response(conn)
end

defp remove_user_avatar({:ok, user}) do
Accounts.remove_user_avatar(user)
end

defp remove_user_avatar(error), do: error

defp send_delete_response({:ok, user}, conn) do
conn
|> put_status(:ok)
|> json(%{
status: "success",
message: "Avatar deleted successfully",
data: %{user_id: user.id}
})
end

defp send_delete_response({:error, reason}, conn) do
{status, message} =
case reason do
%Ecto.Changeset{} -> {:unprocessable_entity, "Avatar deletion failed"}
_ -> {:unprocessable_entity, "Deletion failed"}
end

conn |> put_status(status) |> json(%{status: "error", message: message})
end

swagger_path :delete_avatar do
delete("/v1/users/{id}/avatar")
summary("Delete user avatar")
description("Delete the avatar image for a specific user")
produces("application/json")
tag("Uploaders")
security([%{Bearer: []}])

parameters do
id(:path, :string, "User ID", required: true)
end

response(200, "Success", Schema.ref(:AvatarDeleteSuccess))
response(422, "Deletion failed", Schema.ref(:ErrorResponse))
end

def swagger_definitions do
%{
AvatarUploadSuccess:
swagger_schema do
title("Avatar Upload Success Response")
description("Successful avatar upload response")
type(:object)

properties do
status(:string, "Response status", example: "success")
message(:string, "Success message", example: "Avatar uploaded successfully")
data(Schema.ref(:AvatarData))
end

required([:status, :message, :data])
end,
AvatarData:
swagger_schema do
title("Avatar Data")
description("Avatar upload data")
type(:object)

properties do
avatar_url(:string, "URL of the uploaded avatar",
example: "/this/is/an/xXxXxXxX-xxxx-xxxx-xxxx-xxxxxxxxxxxx/example.jpg"
)

user_id(:string, "User UUID", example: "xXxXxXxX-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
end

required([:avatar_url, :user_id])
end,
ErrorResponse:
swagger_schema do
title("Error Response")
description("Error response format")
type(:object)

properties do
status(:string, "Response status", example: "error")
message(:string, "Error message", example: "User not found")
end

required([:status, :message])
end,
AvatarDeleteSuccess:
swagger_schema do
title("Successful Avatar Deletion")
description("Successful avatar deletion response")
type(:object)

properties do
status(:string, "Response status", example: "success")
message(:string, "Success message", example: "Avatar deleted successfully")
data(Schema.ref(:DeleteData))
end

required([:status, :message, :data])
end,
DeleteData:
swagger_schema do
title("Delete Data")
description("Avatar deletion data")
type(:object)

properties do
user_id(:string, "User UUID", example: "xXxXxXxX-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
end

required([:user_id])
end
}
end
end
3 changes: 3 additions & 0 deletions lib/atlas_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ defmodule AtlasWeb.Router do

pipe_through :auth

post "/users/:id/avatar", UserController, :upload_avatar
delete "/users/:id/avatar", UserController, :delete_avatar

scope "/auth" do
post "/sign_out", AuthController, :sign_out
get "/me", AuthController, :me
Expand Down
8 changes: 8 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ defmodule Atlas.MixProject do
{:xlsx_reader, "~> 0.8.8"},
{:igniter, "~> 0.5", only: [:dev]},

# uploads
{:waffle, "~> 1.1"},
{:waffle_ecto, "~> 0.0.12"},
{:ex_aws, "~> 2.1.2"},
{:ex_aws_s3, "~> 2.0"},
{:hackney, "~> 1.9"},
{:sweet_xml, "~> 0.6"},

# monitoring
{:telemetry_metrics, "~> 1.0"},
{:telemetry_poller, "~> 1.0"},
Expand Down
Loading