Skip to content

Commit

Permalink
Create user auth tokens & source read apis
Browse files Browse the repository at this point in the history
  • Loading branch information
Loïc Knuchel committed Nov 11, 2023
1 parent 5fc0d94 commit f763a78
Show file tree
Hide file tree
Showing 27 changed files with 363 additions and 79 deletions.
2 changes: 1 addition & 1 deletion backend/.credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
{Credo.Check.Readability.FunctionNames, []},
{Credo.Check.Readability.LargeNumbers, []},
# keep in sync with line_length in .formatter.exs
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 140]},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 180]},
{Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleDoc, []},
{Credo.Check.Readability.ModuleNames, []},
Expand Down
2 changes: 1 addition & 1 deletion backend/.formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
subdirectories: ["priv/*/migrations"],
# keep in sync with Credo.Check.Readability.MaxLineLength in .credo.exs
line_length: 140
line_length: 180
]
60 changes: 58 additions & 2 deletions backend/lib/azimutt/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Azimutt.Accounts do
@moduledoc "The Accounts context."
import Ecto.Query, warn: false
alias Azimutt.Repo
alias Azimutt.Accounts.{User, UserNotifier, UserProfile, UserToken}
alias Azimutt.Accounts.{User, UserAuthToken, UserNotifier, UserProfile, UserToken}
alias Azimutt.Organizations
alias Azimutt.Organizations.OrganizationMember
alias Azimutt.Tracking
Expand Down Expand Up @@ -266,6 +266,51 @@ defmodule Azimutt.Accounts do
|> Repo.update()
end

## Auth tokens

def list_auth_tokens(%User{} = current_user, now) do
UserAuthToken
|> where([t], t.user_id == ^current_user.id and is_nil(t.deleted_at) and (is_nil(t.expire_at) or t.expire_at > ^now))
|> Repo.all()
end

def change_auth_token(%User{} = current_user, attrs \\ %{}) do
%UserAuthToken{}
|> UserAuthToken.create_changeset(%{
name: attrs["name"],
expire_at: attrs["expire_at"],
user_id: current_user.id
})
end

def create_auth_token(%User{} = current_user, now, attrs) do
%UserAuthToken{}
|> UserAuthToken.create_changeset(%{
name: attrs["name"],
expire_at:
case attrs["expire_at"] do
"hour" -> Timex.shift(now, hours: 1)
"day" -> Timex.shift(now, days: 1)
"month" -> Timex.shift(now, months: 1)
_ -> nil
end,
user_id: current_user.id
})
|> Repo.insert()
end

def delete_auth_token(token_id, %User{} = current_user, now) do
UserAuthToken
|> where([t], t.id == ^token_id and t.user_id == ^current_user.id and is_nil(t.deleted_at))
|> Repo.one()
|> Result.from_nillable()
|> Result.flat_map(fn token ->
token
|> UserAuthToken.delete_changeset(now)
|> Repo.update()
end)
end

## Session

def generate_user_session_token(user) do
Expand All @@ -276,7 +321,18 @@ defmodule Azimutt.Accounts do

def get_user_by_session_token(token) do
{:ok, query} = UserToken.verify_session_token_query(token)
Repo.one(query)
Repo.one(query) |> Result.from_nillable()
end

def get_user_by_auth_token(token_id, now) do
UserAuthToken
|> where([t], t.id == ^token_id and is_nil(t.deleted_at) and (is_nil(t.expire_at) or t.expire_at > ^now))
|> Repo.one()
|> Result.from_nillable()
|> Result.flat_map(fn token ->
token |> UserAuthToken.access_changeset(now) |> Repo.update()
User |> Repo.get(token.user_id) |> Result.from_nillable()
end)
end

def delete_session_token(token) do
Expand Down
35 changes: 35 additions & 0 deletions backend/lib/azimutt/accounts/user_auth_token.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule Azimutt.Accounts.UserAuthToken do
@moduledoc "Auth tokens allowing to identify a user simply by passing this token"
use Ecto.Schema
use Azimutt.Schema
import Ecto.Changeset
alias Azimutt.Accounts.User

schema "user_auth_tokens" do
belongs_to :user, User
field :name, :string
field :nb_access, :integer
field :last_access, :utc_datetime_usec
field :expire_at, :utc_datetime_usec
timestamps(updated_at: false)
field :deleted_at, :utc_datetime_usec
end

def create_changeset(token, attrs) do
token
|> cast(attrs, [:name, :expire_at])
|> put_change(:user_id, attrs.user_id)
|> put_change(:nb_access, 0)
|> validate_required([:name])
end

def access_changeset(token, now) do
token
|> cast(%{last_access: now, nb_access: token.nb_access + 1}, [:last_access, :nb_access])
end

def delete_changeset(token, now) do
token
|> cast(%{deleted_at: now}, [:deleted_at])
end
end
5 changes: 1 addition & 4 deletions backend/lib/azimutt/accounts/user_token.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
defmodule Azimutt.Accounts.UserToken do
@moduledoc """
base user token module generate by `mix phx.gen.auth`
"""
@moduledoc "base user token module generate by `mix phx.gen.auth`"
use Ecto.Schema
use Azimutt.Schema
import Ecto.Query

alias Azimutt.Accounts.User
alias Azimutt.Accounts.UserToken

Expand Down
17 changes: 17 additions & 0 deletions backend/lib/azimutt/projects.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Azimutt.Projects do
alias Azimutt.Organizations.OrganizationMember
alias Azimutt.Projects.Project
alias Azimutt.Projects.Project.Storage
alias Azimutt.Projects.ProjectFile
alias Azimutt.Projects.ProjectToken
alias Azimutt.Repo
alias Azimutt.Tracking
Expand Down Expand Up @@ -115,6 +116,22 @@ defmodule Azimutt.Projects do
end
end

def get_project_content(%Project{} = project) do
if project.storage_kind == Storage.remote() do
# FIXME: handle spaces in name
file_url = ProjectFile.url({project.file, project}, signed: true)

if Application.get_env(:waffle, :storage) == Waffle.Storage.Local do
File.read("./#{file_url}")
else
# FIXME: improve http error handling
{:ok, HTTPoison.get!(file_url).body}
end
else
{:error, :not_remote}
end
end

def list_project_tokens(project_id, %User{} = current_user, now) do
project_query()
|> where([p, _, om], p.id == ^project_id and p.storage_kind == :remote and om.user_id == ^current_user.id)
Expand Down
3 changes: 1 addition & 2 deletions backend/lib/azimutt/services/cockpit_srv.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,7 @@ defmodule Azimutt.Services.CockpitSrv do
admins: User |> where([u], u.is_admin == true) |> Repo.aggregate(:count),
organizations: Organization |> Repo.aggregate(:count),
np_organizations: Organization |> where([o], o.is_personal == false) |> Repo.aggregate(:count),
active_organizations:
Event |> where([e], e.created_at > ^last_30_days) |> select([e], count(e.organization_id, :distinct)) |> Repo.one(),
active_organizations: Event |> where([e], e.created_at > ^last_30_days) |> select([e], count(e.organization_id, :distinct)) |> Repo.one(),
projects: Project |> Repo.aggregate(:count),
active_projects: Event |> where([e], e.created_at > ^last_30_days) |> select([e], count(e.project_id, :distinct)) |> Repo.one(),
events: Event |> Repo.aggregate(:count),
Expand Down
3 changes: 1 addition & 2 deletions backend/lib/azimutt/tracking.ex
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,7 @@ defmodule Azimutt.Tracking do
stripe_customer_id: org.stripe_customer_id,
stripe_subscription_id: org.stripe_subscription_id,
is_personal: org.is_personal,
clever_cloud:
if(Ecto.assoc_loaded?(org.clever_cloud_resource) && org.clever_cloud_resource, do: org.clever_cloud_resource.id, else: nil),
clever_cloud: if(Ecto.assoc_loaded?(org.clever_cloud_resource) && org.clever_cloud_resource, do: org.clever_cloud_resource.id, else: nil),
heroku: if(Ecto.assoc_loaded?(org.heroku_resource) && org.heroku_resource, do: org.heroku_resource.id, else: nil),
members: if(Ecto.assoc_loaded?(org.members), do: org.members |> length, else: nil),
projects: if(Ecto.assoc_loaded?(org.projects), do: org.projects |> length, else: nil),
Expand Down
3 changes: 1 addition & 2 deletions backend/lib/azimutt/utils/page.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ defmodule Azimutt.Utils.Page do
query
|> Map.filter(fn {k, _v} -> k |> String.starts_with?("#{prefix}f-") end)
|> Mapx.map_keys(fn k -> k |> String.replace_leading("#{prefix}f-", "") end),
sort:
(query["#{prefix}sort"] || opts[:sort] || "") |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.filter(fn s -> s != "" end)
sort: (query["#{prefix}sort"] || opts[:sort] || "") |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.filter(fn s -> s != "" end)
}
end

Expand Down
3 changes: 1 addition & 2 deletions backend/lib/azimutt_web/admin/project/project_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ defmodule AzimuttWeb.Admin.ProjectController do
conn
|> render("show.html",
project: project,
activity:
Dataset.chartjs_daily_data([Admin.daily_project_activity(project) |> Dataset.from_values("Daily events")], start_stats, now),
activity: Dataset.chartjs_daily_data([Admin.daily_project_activity(project) |> Dataset.from_values("Daily events")], start_stats, now),
events: Admin.get_project_events(project, events_page)
)
end
Expand Down
22 changes: 22 additions & 0 deletions backend/lib/azimutt_web/components/icon.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ defmodule AzimuttWeb.Components.Icon do
"shield-check" -> shield_check(assigns)
"shopping-cart" -> shopping_cart(assigns)
"squares-plus" -> squares_plus(assigns)
"trash" -> trash(assigns)
"user-circle" -> user_circle(assigns)
"user-group" -> user_group(assigns)
"x-circle" -> x_circle(assigns)
Expand Down Expand Up @@ -1387,6 +1388,27 @@ defmodule AzimuttWeb.Components.Icon do
end
end

def trash(assigns) do
classes = if assigns[:class], do: " #{assigns[:class]}", else: ""

case assigns[:kind] do
"outline" ->
~H"""
<.outline classes={classes}><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /></.outline>
"""

"solid" ->
~H"""
<.solid classes={classes}><path fill-rule="evenodd" d="M16.5 4.478v.227a48.816 48.816 0 013.878.512.75.75 0 11-.256 1.478l-.209-.035-1.005 13.07a3 3 0 01-2.991 2.77H8.084a3 3 0 01-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 01-.256-1.478A48.567 48.567 0 017.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 013.369 0c1.603.051 2.815 1.387 2.815 2.951zm-6.136-1.452a51.196 51.196 0 013.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 00-6 0v-.113c0-.794.609-1.428 1.364-1.452zm-.355 5.945a.75.75 0 10-1.5.058l.347 9a.75.75 0 101.499-.058l-.346-9zm5.48.058a.75.75 0 10-1.498-.058l-.347 9a.75.75 0 001.5.058l.345-9z" clip-rule="evenodd" /></.solid>
"""

_ ->
~H"""
<.mini classes={classes}><path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z" clip-rule="evenodd" /></.mini>
"""
end
end

def user_circle(assigns) do
classes = if assigns[:class], do: " #{assigns[:class]}", else: ""

Expand Down
41 changes: 41 additions & 0 deletions backend/lib/azimutt_web/controllers/api/source_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule AzimuttWeb.Api.SourceController do
use AzimuttWeb, :controller
use PhoenixSwagger
alias Azimutt.Projects
alias Azimutt.Projects.Project
alias Azimutt.Utils.Result
alias AzimuttWeb.Utils.CtxParams
action_fallback AzimuttWeb.Api.FallbackController

def index(conn, %{"organization_id" => _organization_id, "project_id" => project_id} = params) do
current_user = conn.assigns.current_user
ctx = CtxParams.from_params(params)

with {:ok, %Project{} = project} <- Projects.get_project(project_id, current_user),
{:ok, content} <- Projects.get_project_content(project) |> Result.flat_map(fn c -> Jason.decode(c) end),
do: conn |> render("index.json", sources: content["sources"], ctx: ctx)
end

def show(conn, %{"organization_id" => _organization_id, "project_id" => project_id, "source_id" => source_id} = params) do
current_user = conn.assigns.current_user
ctx = CtxParams.from_params(params)

with {:ok, %Project{} = project} <- Projects.get_project(project_id, current_user),
{:ok, content} <- Projects.get_project_content(project) |> Result.flat_map(fn c -> Jason.decode(c) end),
{:ok, source} <- content["sources"] |> Enum.find(fn s -> s["id"] == source_id end) |> Result.from_nillable(),
do: conn |> render("show.json", source: source, ctx: ctx)
end

def update(conn, %{"organization_organization_id" => _organization_id, "project_id" => _project_id, "source_id" => _source_id} = _params) do
# FIXME
conn |> send_resp(:not_found, "WIP")
end

def create(conn, %{"organization_organization_id" => _organization_id, "project_id" => _project_id} = _params) do
conn |> send_resp(:not_found, "WIP")
end

def delete(conn, %{"organization_organization_id" => _organization_id, "project_id" => _project_id, "source_id" => _source_id}) do
conn |> send_resp(:not_found, "WIP")
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ defmodule AzimuttWeb.OrganizationMemberController do
end

with {:ok, %Organization{} = organization} <- Organizations.get_organization(organization_id, current_user) do
organization_invitation_changeset =
OrganizationInvitation.create_changeset(%OrganizationInvitation{}, %{}, organization.id, current_user, now)

organization_invitation_changeset = OrganizationInvitation.create_changeset(%OrganizationInvitation{}, %{}, organization.id, current_user, now)
render_index(conn, organization, organization_invitation_changeset)
end
end
Expand Down
11 changes: 9 additions & 2 deletions backend/lib/azimutt_web/controllers/user_auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,18 @@ defmodule AzimuttWeb.UserAuth do
and remember me token.
"""
def fetch_current_user(conn, _opts) do
auth_token = conn.params["auth-token"]
{user_token, conn} = ensure_user_token(conn)
user = user_token && Accounts.get_user_by_session_token(user_token)

user =
user |> Azimutt.Repo.preload(:profile) |> Azimutt.Repo.preload(organizations: [:clever_cloud_resource, :heroku_resource, :projects])
cond do
user_token -> Accounts.get_user_by_session_token(user_token)
auth_token -> Accounts.get_user_by_auth_token(auth_token, DateTime.utc_now())
true -> {:ok, nil}
end
|> Result.or_else(nil)

user = user |> Azimutt.Repo.preload(:profile) |> Azimutt.Repo.preload(organizations: [:clever_cloud_resource, :heroku_resource, :projects])

assign(conn, :current_user, user)
end
Expand Down
Loading

0 comments on commit f763a78

Please sign in to comment.