Skip to content

Commit

Permalink
Extend live stream visibility options: public, unlisted, private
Browse files Browse the repository at this point in the history
  • Loading branch information
ku1ik committed May 13, 2024
1 parent b34c641 commit b309979
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 45 deletions.
1 change: 1 addition & 0 deletions lib/asciinema/authorization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Asciinema.Authorization do
alias Asciinema.Streaming.LiveStream

defmodule Policy do
def can?(_user, :show, %LiveStream{visibility: v}) when v in [:public, :unlisted], do: true
def can?(nil, _action, _thing), do: false
def can?(%User{is_admin: true}, _action, _thing), do: true
def can?(_user, :make_featured, %Asciicast{}), do: false
Expand Down
4 changes: 2 additions & 2 deletions lib/asciinema/streaming.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ defmodule Asciinema.Streaming do
def list_public_live_streams(owner, limit \\ 4) do
owner
|> live_streams_q(limit)
|> where([s], not s.private)
|> where([s], s.visibility == :public)
|> Repo.all()
end

Expand Down Expand Up @@ -85,7 +85,7 @@ defmodule Asciinema.Streaming do
|> cast(attrs, [
:title,
:description,
:private,
:visibility,
:theme_name,
:theme_prefer_original,
:buffer_time,
Expand Down
2 changes: 1 addition & 1 deletion lib/asciinema/streaming/live_stream.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Asciinema.Streaming.LiveStream do
schema "live_streams" do
field :public_token, :string
field :producer_token, :string
field :private, :boolean, default: true
field :visibility, Ecto.Enum, values: [:private, :unlisted, :public], default: :unlisted
field :cols, :integer
field :rows, :integer
field :online, :boolean
Expand Down
1 change: 0 additions & 1 deletion lib/asciinema_web/controllers/live_stream/card.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
<div class="info">
<h3>
<.link href={~p"/s/#{@stream}"}><%= title(@stream) %></.link>
<.secret_badge :if={@stream.private} />
<span class="duration"><%= duration(@stream) %></span>
</h3>

Expand Down
56 changes: 56 additions & 0 deletions lib/asciinema_web/controllers/live_stream/edit.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,62 @@
<.form :let={f} for={@changeset} action={~p"/s/#{@changeset.data}"}>
<legend>Live stream settings</legend>

<div class="form-group row">
<.label for={f[:visibility]} class="col-3 col-form-label">
Visibility
</.label>
<div class="col-9 visibility-radios">
<div class="form-check">
<input
type="radio"
name="live_stream[visibility]"
id="live_stream_visibility_public"
value="public"
checked={f[:visibility].value == :public}
class="form-check-input"
/>
<label for="live_stream_visibility_public" class="form-check-label">
Public
</label>
<small class="form-text text-muted">
Anyone can view, displayed on your public profile when live
</small>
</div>
<div class="form-check">
<input
type="radio"
name="live_stream[visibility]"
id="live_stream_visibility_unlisted"
value="unlisted"
checked={f[:visibility].value == :unlisted}
class="form-check-input"
/>
<label for="live_stream_visibility_unlisted" class="form-check-label">
Unlisted
</label>
<small class="form-text text-muted">
Only people with the link can view
</small>
</div>
<div class="form-check">
<input
type="radio"
name="live_stream[visibility]"
id="live_stream_visibility_private"
value="private"
checked={f[:visibility].value == :private}
class="form-check-input"
/>
<label for="live_stream_visibility_private" class="form-check-label">
Private
</label>
<small class="form-text text-muted">
Only accessible by you
</small>
</div>
</div>
</div>

<div class="form-group row">
<.label for={f[:title]} class="col-sm-4 col-md-3 col-lg-3 col-form-label">Title</.label>
<div class="col-sm-8 col-md-9 col-lg-9">
Expand Down

This file was deleted.

5 changes: 4 additions & 1 deletion lib/asciinema_web/controllers/live_stream/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
<small>
by
<.link navigate={author_profile_path(@stream)}><%= author_username(@stream) %></.link>
<.secret_badge :if={@stream.private} />
<.visibility_badge
:if={owned_by_current_user?(@stream, @conn)}
visibility={@stream.visibility}
/>
</small>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<span
:if={@visibility == :public}
class="special-label"
title="Anyone can view, displayed on your public profile when live"
>
public
</span>
<span
:if={@visibility == :unlisted}
class="special-label"
title="Only people with the link can view"
>
unlisted
</span>
<span :if={@visibility == :private} class="special-label" title="Only accessible by you">
private
</span>
33 changes: 15 additions & 18 deletions lib/asciinema_web/controllers/live_stream_controller.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
defmodule AsciinemaWeb.LiveStreamController do
use AsciinemaWeb, :controller
alias Asciinema.{Authorization, Recordings, Streaming}
alias AsciinemaWeb.{FallbackController, LiveStreamHTML, PlayerOpts}
alias AsciinemaWeb.{Auth, FallbackController, LiveStreamHTML, PlayerOpts}

plug :load_stream when action in [:show, :edit, :update]
plug :require_current_user_when_private when action == :show
plug :require_current_user when action in [:edit, :update]
plug :authorize, :stream when action in [:edit, :update]
plug :authorize, :stream when action in [:show, :edit, :update]

def show(conn, params) do
stream = conn.assigns.stream
Expand Down Expand Up @@ -44,23 +45,11 @@ defmodule AsciinemaWeb.LiveStreamController do
PlayerOpts.parse(params, :live_stream)
end

@actions [
:edit,
:make_private,
:make_public
]

defp stream_actions(stream, user) do
@actions
|> Enum.filter(&action_applicable?(&1, stream))
|> Enum.filter(&Authorization.can?(user, &1, stream))
end

defp action_applicable?(action, stream) do
case action do
:edit -> true
:make_private -> !stream.private
:make_public -> stream.private
if Authorization.can?(user, :edit, stream) do
[:edit]
else
[]
end
end

Expand All @@ -75,4 +64,12 @@ defmodule AsciinemaWeb.LiveStreamController do
|> halt()
end
end

defp require_current_user_when_private(conn, _opts) do
if conn.assigns.stream.visibility == :private do
Auth.require_current_user(conn, [])
else
conn
end
end
end
4 changes: 4 additions & 0 deletions lib/asciinema_web/controllers/live_stream_html.ex
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,8 @@ defmodule AsciinemaWeb.LiveStreamHTML do
defp cols(stream), do: stream.cols || 80

defp rows(stream), do: stream.rows || 24

defp owned_by_current_user?(stream, conn) do
conn.assigns[:current_user] && conn.assigns[:current_user].id == stream.user_id
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule Asciinema.Repo.Migrations.AddVisibilityToLiveStreams do
use Ecto.Migration

def up do
execute("CREATE TYPE live_stream_visibility AS ENUM ('private', 'unlisted', 'public')")

alter table(:live_streams) do
add :visibility, :live_stream_visibility, null: false, default: "unlisted"
end

execute "UPDATE live_streams SET visibility = 'public' WHERE NOT private"

alter table(:live_streams) do
remove :private
end
end

def down do
alter table(:live_streams) do
add :private, :boolean, null: false, default: true
end

execute "UPDATE live_streams SET private = FALSE WHERE visibility = 'public'"

alter table(:live_streams) do
remove :visibility
end

execute("DROP TYPE live_stream_visibility")
end
end
47 changes: 38 additions & 9 deletions test/controllers/live_stream_controller_test.exs
Original file line number Diff line number Diff line change
@@ -1,28 +1,57 @@
defmodule Asciinema.LiveStreamControllerTest do
use AsciinemaWeb.ConnCase
import Asciinema.Factory
alias Asciinema.Authorization

describe "show" do
test "HTML, private stream", %{conn: conn} do
stream = insert(:live_stream, private: true)
test "public stream", %{conn: conn} do
stream = insert(:live_stream, visibility: :public)

conn_2 = get(conn, ~p"/s/#{stream}")

assert html_response(conn_2, 200) =~ "createPlayer"
assert response_content_type(conn_2, :html)
end

test "HTML, public stream", %{conn: conn} do
stream = insert(:live_stream, private: false)
test "unlisted stream", %{conn: conn} do
stream = insert(:live_stream, visibility: :unlisted)

conn_2 = get(conn, ~p"/s/#{stream}")

assert html_response(conn_2, 200) =~ "createPlayer"
assert response_content_type(conn_2, :html)
end

test "HTML, streaming instructions", %{conn: conn} do
test "private stream, unauthenticated", %{conn: conn} do
user = insert(:user)
stream = insert(:live_stream, visibility: :private, user: user)

conn_2 = get(conn, ~p"/s/#{stream}")

assert redirected_to(conn_2, 302) == ~p"/login/new"
end

test "private stream, as non-owner", %{conn: conn} do
stream = insert(:live_stream, visibility: :private)
user = insert(:user)
conn = log_in(conn, user)

conn_2 = get(conn, ~p"/s/#{stream}")

assert html_response(conn_2, 403)
end

test "private stream, as owner", %{conn: conn} do
user = insert(:user)
stream = insert(:live_stream, visibility: :private, user: user)
conn = log_in(conn, user)

conn_2 = get(conn, ~p"/s/#{stream}")

assert html_response(conn_2, 200) =~ "createPlayer"
assert response_content_type(conn_2, :html)
end

test "streaming instructions", %{conn: conn} do
user = insert(:user)
stream = insert(:live_stream, user: user)

Expand Down Expand Up @@ -58,9 +87,9 @@ defmodule Asciinema.LiveStreamControllerTest do
test "requires owner", %{conn: conn, stream: stream} do
conn = log_in(conn, insert(:user))

assert_raise(Authorization.ForbiddenError, fn ->
get(conn, ~p"/s/#{stream}/edit")
end)
conn = get(conn, ~p"/s/#{stream}/edit")

assert html_response(conn, 403) =~ "access"
end

test "displays form", %{conn: conn, stream: stream, user: user} do
Expand Down
15 changes: 8 additions & 7 deletions test/controllers/recording_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,16 @@ defmodule Asciinema.RecordingControllerTest do

test "requires logged in user", %{conn: conn, asciicast: asciicast} do
conn = get(conn, Routes.recording_path(conn, :edit, asciicast))
assert redirected_to(conn, 302) == "/login/new"

assert redirected_to(conn, 302) == ~p"/login/new"
end

test "requires author", %{conn: conn, asciicast: asciicast} do
conn = log_in(conn, insert(:user))

assert_raise(Asciinema.Authorization.ForbiddenError, fn ->
get(conn, Routes.recording_path(conn, :edit, asciicast))
end)
conn = get(conn, Routes.recording_path(conn, :edit, asciicast))

assert html_response(conn, 403) =~ "access"
end

test "displays form", %{conn: conn, asciicast: asciicast, user: user} do
Expand Down Expand Up @@ -209,9 +210,9 @@ defmodule Asciinema.RecordingControllerTest do
test "requires author", %{conn: conn, asciicast: asciicast} do
conn = log_in(conn, insert(:user))

assert_raise(Asciinema.Authorization.ForbiddenError, fn ->
delete(conn, Routes.recording_path(conn, :delete, asciicast))
end)
conn = delete(conn, Routes.recording_path(conn, :delete, asciicast))

assert html_response(conn, 403) =~ "access"
end

test "removes and redirects", %{conn: conn, asciicast: asciicast, user: user} do
Expand Down

0 comments on commit b309979

Please sign in to comment.