Skip to content
Merged
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
57 changes: 57 additions & 0 deletions lib/textbin/organizations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,8 @@ defmodule Textbin.Organizations do
end

defp record_audit(organization_id, actor_user_id, action, target_type, target_id, metadata) do
metadata = snapshot_audit_metadata(metadata, actor_user_id, target_type, target_id)

%AuditEvent{}
|> AuditEvent.changeset(%{
organization_id: organization_id,
Expand All @@ -1361,6 +1363,61 @@ defmodule Textbin.Organizations do
end
end

# Audit labels are snapshots: mutable or deleted records must not rewrite historical context.
defp snapshot_audit_metadata(metadata, actor_user_id, target_type, target_id) do
metadata
|> put_audit_label("actor_email", user_email(actor_user_id))
|> put_target_audit_label(target_type, target_id)
|> put_workspace_audit_label()
end

defp put_target_audit_label(metadata, "user", target_id),
do: put_audit_label(metadata, "target_email", user_email(target_id))

defp put_target_audit_label(metadata, "workspace", target_id) do
name =
case Repo.get(Workspace, target_id) do
%Workspace{name: name} -> name
nil -> metadata["name"]
end

put_audit_label(metadata, "target_name", name)
end

defp put_target_audit_label(metadata, "organization", target_id) do
name =
case Repo.get(Organization, target_id) do
%Organization{name: name} -> name
nil -> nil
end

put_audit_label(metadata, "target_name", name)
end

defp put_target_audit_label(metadata, _target_type, _target_id), do: metadata

defp put_workspace_audit_label(%{"workspace_id" => workspace_id} = metadata) do
name =
case Repo.get(Workspace, workspace_id) do
%Workspace{name: name} -> name
nil -> nil
end

put_audit_label(metadata, "workspace_name", name)
end

defp put_workspace_audit_label(metadata), do: metadata

defp put_audit_label(metadata, _key, nil), do: metadata
defp put_audit_label(metadata, key, value), do: Map.put_new(metadata, key, value)

defp user_email(user_id) do
case Repo.get(User, user_id) do
%User{email: email} -> email
nil -> nil
end
end

defp record_role_change_audit(
_organization_id,
_actor_user_id,
Expand Down
39 changes: 38 additions & 1 deletion lib/textbin_web/components/layouts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ defmodule TextbinWeb.Layouts do
<.mobile_more_navigation
scope={@current_scope}
workspaces={@navigation_workspaces}
active={@active_navigation}
/>
</div>
</div>
Expand Down Expand Up @@ -179,8 +180,15 @@ defmodule TextbinWeb.Layouts do
<button
type="button"
id="mobile-navigation-more"
class="flex min-h-14 min-w-0 flex-col items-center justify-center gap-1 rounded-xl px-1 py-2 text-[0.6875rem] font-semibold text-base-content/60 transition hover:bg-base-200 hover:text-base-content aria-expanded:bg-primary/10 aria-expanded:text-primary"
class={[
"flex min-h-14 min-w-0 flex-col items-center justify-center gap-1 rounded-xl px-1 py-2 text-[0.6875rem] font-semibold transition aria-expanded:bg-primary/10 aria-expanded:text-primary",
if(more_navigation_active?(@active),
do: "bg-primary/10 text-primary",
else: "text-base-content/60 hover:bg-base-200 hover:text-base-content"
)
]}
aria-label="Open more navigation"
aria-current={more_navigation_active?(@active) && "page"}
aria-controls="mobile-navigation-dialog"
aria-expanded="false"
data-navigation-dialog-open
Expand Down Expand Up @@ -218,6 +226,7 @@ defmodule TextbinWeb.Layouts do

attr :scope, :map, required: true
attr :workspaces, :list, required: true
attr :active, :any, default: nil

defp mobile_more_navigation(assigns) do
~H"""
Expand Down Expand Up @@ -276,6 +285,20 @@ defmodule TextbinWeb.Layouts do
>
<.icon name="hero-squares-2x2" class="size-4.5" /> Manage workspaces
</.link>
<.link
:if={organization_owner?(@scope)}
navigate={organization_audit_log_path(@scope.organization)}
aria-current={@active == {:organization, :audit_log} && "page"}
class={[
"flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm font-medium transition",
if(@active == {:organization, :audit_log},
do: "bg-primary/10 text-primary",
else: "text-base-content/65 hover:bg-base-200 hover:text-base-content"
)
]}
>
<.icon name="hero-shield-check" class="size-4.5" /> Audit log
</.link>
<.link
navigate={~p"/orgs"}
class="flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm font-medium text-base-content/65 transition hover:bg-base-200 hover:text-base-content"
Expand Down Expand Up @@ -370,6 +393,13 @@ defmodule TextbinWeb.Layouts do
label="Manage workspaces"
active={@active == {:organization, :workspaces}}
/>
<.sidebar_link
:if={organization_owner?(@scope)}
navigate={organization_audit_log_path(@scope.organization)}
icon="hero-shield-check"
label="Audit log"
active={@active == {:organization, :audit_log}}
/>
<.sidebar_link
navigate={organization_settings_path(@scope.organization)}
icon="hero-cog-6-tooth"
Expand Down Expand Up @@ -603,13 +633,20 @@ defmodule TextbinWeb.Layouts do
defp active_workspace?(%{workspace: %{id: workspace_id}}, %{id: workspace_id}), do: true
defp active_workspace?(_scope, _workspace), do: false

defp more_navigation_active?(active),
do: active in [{:organization, :workspaces}, {:organization, :audit_log}]

defp organization_owner?(%{organization_membership: %{role: "owner"}}), do: true
defp organization_owner?(_scope), do: false

defp organization_menu_id("sidebar-navigation"), do: "organization-menu"
defp organization_menu_id(id), do: "#{id}-organization-menu"

defp organization_menu_panel_id("sidebar-navigation"), do: "organization-menu-panel"
defp organization_menu_panel_id(id), do: "#{id}-organization-menu-panel"

defp organization_path(organization), do: "/o/#{organization.slug}"
defp organization_audit_log_path(organization), do: "/o/#{organization.slug}/audit-log"
defp organization_members_path(organization), do: "/o/#{organization.slug}/members"
defp organization_workspaces_path(organization), do: "/o/#{organization.slug}/workspaces"
defp organization_settings_path(organization), do: "/o/#{organization.slug}/settings"
Expand Down
Loading
Loading