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
3 changes: 3 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ config :phoenix, :plug_init_mode, :runtime
config :phoenix_live_view,
# Enable helpful, but potentially expensive runtime checks
enable_expensive_runtime_checks: true

# test config
config :bcrypt_elixir, log_rounds: 4
26 changes: 26 additions & 0 deletions lib/atlas/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -488,4 +488,30 @@ defmodule Atlas.Accounts do
Guardian.DB.revoke_all(user_session.id)
Repo.delete(user_session)
end

def get_active_user!(id), do: get_user!(id)

def update_user_password(user, attrs) do
update_user_password(
user,
Map.get(attrs, "current_password") || Map.get(attrs, :current_password),
attrs
)
end

def update_user_profile(user, attrs) do
user
|> User.profile_changeset(attrs)
|> Repo.update()
end

def delete_user_account(user) do
user
|> Ecto.Changeset.change(is_active: false)
|> Repo.update()
end

def authenticate_user(email, password) do
get_user_by_email_and_password(email, password)
end
end
27 changes: 27 additions & 0 deletions lib/atlas/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ 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 :is_active, :boolean, default: true
field :gender, :string
field :birth_date, :date
field :profile_picture, :string

timestamps(type: :utc_datetime)
end
Expand Down Expand Up @@ -163,4 +167,27 @@ defmodule Atlas.Accounts.User do
add_error(changeset, :current_password, "is not valid")
end
end

def changeset(user, attrs) do
registration_changeset(user, attrs)
end

def profile_changeset(user, attrs) do
fields = [:name, :email, :type, :gender, :birth_date]

fields =
if Map.has_key?(attrs, "profile_picture"), do: fields ++ [:profile_picture], else: fields

user
|> cast(attrs, fields)
|> validate_required([:name, :email])
|> validate_inclusion(:gender, ["male", "female", "other"])
|> validate_change(:birth_date, fn :birth_date, date ->
if date && Date.compare(date, Date.utc_today()) == :gt do
[birth_date: "cannot be in the future"]
else
[]
end
end)
end
end
15 changes: 15 additions & 0 deletions lib/atlas/students/student.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Atlas.Students.Student do
use Ecto.Schema

@moduledoc """
Schema for students, representing a student entity in the system.
"""

schema "students" do
field :name, :string

belongs_to :user, Atlas.Accounts.User

timestamps()
end
end
25 changes: 25 additions & 0 deletions lib/atlas_web/auth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule AtlasWeb.Auth do
@moduledoc """
Authentication helper functions.
"""

import Plug.Conn
alias Atlas.Accounts

def get_current_user(conn) do
user_id = get_session(conn, :user_id)

case user_id do
nil -> nil
id -> Accounts.get_active_user!(id)
end
rescue
Ecto.NoResultsError -> nil
end

def logout_user(conn) do
conn
|> delete_session(:user_id)
|> configure_session(drop: true)
end
end
151 changes: 151 additions & 0 deletions lib/atlas_web/controllers/user_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
defmodule AtlasWeb.UserController do
use AtlasWeb, :controller

alias Atlas.Accounts
alias AtlasWeb.Auth

plug :authenticate_user when action in [:update_password, :update_profile, :delete_account]

plug :authorize_user when action in [:update_password, :update_profile, :delete_account]

def update_password(conn, %{"password" => password_params}) do
current_user = conn.assigns[:current_user]

case Accounts.update_user_password(current_user, password_params) do
{:ok, _user} ->
conn
|> json(%{success: true, message: "Password updated successfully"})

{:error, %Ecto.Changeset{} = changeset} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{
success: false,
message: "Failed to update password",
errors: format_changeset_errors(changeset)
})
end
end

def update_profile(conn, %{"profile" => profile_params}) do
current_user = conn.assigns[:current_user]

profile_params = handle_profile_picture_upload(profile_params)

case Accounts.update_user_profile(current_user, profile_params) do
{:ok, user} ->
conn
|> json(%{
success: true,
message: "Profile updated successfully",
user: %{
id: user.id,
email: user.email,
gender: user.gender,
profile_picture: user.profile_picture,
birth_date: user.birth_date
}
})

{:error, %Ecto.Changeset{} = changeset} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{
success: false,
message: "Failed to update profile",
errors: format_changeset_errors(changeset)
})
end
end

def delete_account(conn, _params) do
current_user = conn.assigns[:current_user]

case Accounts.delete_user_account(current_user) do
{:ok, _user} ->
conn
|> Auth.logout_user()
|> json(%{
success: true,
message: "Account deleted successfully"
})

{:error, %Ecto.Changeset{} = changeset} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{
success: false,
message: "Failed to delete account",
errors: format_changeset_errors(changeset)
})
end
end

defp authenticate_user(conn, _opts) do
case Auth.get_current_user(conn) do
nil ->
conn
|> put_status(:unauthorized)
|> json(%{success: false, message: "Authentication required"})
|> halt()

user ->
assign(conn, :current_user, user)
end
end

defp authorize_user(conn, _opts) do
current_user = conn.assigns[:current_user]
user_id = conn.params["id"]

if current_user.id == user_id do
conn
else
conn
|> put_status(:forbidden)
|> json(%{success: false, message: "Access denied"})
|> halt()
end
end

defp handle_profile_picture_upload(params) do
case params["profile_picture"] do
%Plug.Upload{} = upload ->
case upload_file(upload) do
{:ok, file_url} ->
Map.put(params, "profile_picture", file_url)

{:error, _} ->
Map.delete(params, "profile_picture")
end

nil ->
Map.delete(params, "profile_picture")

_ ->
params
end
end

defp upload_file(%Plug.Upload{filename: filename, path: path}) do
extension = Path.extname(filename)
new_filename = "#{Ecto.UUID.generate()}#{extension}"
destination = Path.join(["priv", "static", "uploads", new_filename])

case File.cp(path, destination) do
:ok ->
{:ok, "/uploads/#{new_filename}"}

{:error, reason} ->
{:error, reason}
end
end

defp format_changeset_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end
end
8 changes: 8 additions & 0 deletions lib/atlas_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ defmodule AtlasWeb.Router do
end
end

scope "/api", AtlasWeb do
pipe_through :api

put "/users/:id/profile", UserController, :update_profile
put "/users/:id/password", UserController, :update_password
delete "/users/:id/account", UserController, :delete_account
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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Atlas.Repo.Migrations.AddIsActiveGenderBirthDateToUsers do
use Ecto.Migration

def change do
alter table(:users) do
add :is_active, :boolean, default: true, null: false
add :gender, :string
add :birth_date, :date
add :profile_picture, :string
end
end
end
Loading