Skip to content

Commit f15fe93

Browse files
committed
feat: implement organization preview functionality
- Added `init_preview/2` function to create a new organization and user for preview contexts. - Introduced `OrgPreviewCallbackController` to handle preview login and session management. - Updated routing to include a new preview path and adjusted user authentication flow for preview contexts. - Enhanced `UserAuth` with methods for signing and verifying preview codes. - Updated `PreviewNav` to manage navigation for preview contexts effectively. - Added tests for the new preview functionality and verification logic.
1 parent e645eda commit f15fe93

File tree

8 files changed

+195
-28
lines changed

8 files changed

+195
-28
lines changed

lib/algora/accounts/accounts.ex

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,8 +413,20 @@ defmodule Algora.Accounts do
413413

414414
def get_last_context_user(%User{} = user) do
415415
case last_context(user) do
416-
"personal" -> user
417-
last_context -> get_user_by_handle(last_context)
416+
"personal" ->
417+
user
418+
419+
"preview/" <> ctx ->
420+
case String.split(ctx, "/") do
421+
[id, _repo_owner, _repo_name] -> get_user(id)
422+
_ -> nil
423+
end
424+
425+
"repo/" <> _repo_full_name ->
426+
user
427+
428+
last_context ->
429+
get_user_by_handle(last_context)
418430
end
419431
end
420432

lib/algora/organizations/organizations.ex

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ defmodule Algora.Organizations do
33
import Ecto.Query
44

55
alias Algora.Accounts.User
6+
alias Algora.Github.TokenPool
67
alias Algora.Organizations.Member
78
alias Algora.Organizations.Org
89
alias Algora.Repo
10+
alias Algora.Workspace
911

1012
def create_organization(params) do
1113
%User{type: :organization}
@@ -228,4 +230,31 @@ defmodule Algora.Organizations do
228230
where: c.client_id == ^org.id and c.contractor_id == u.id
229231
)
230232
end
233+
234+
def init_preview(repo_owner, repo_name) do
235+
token = TokenPool.get_token()
236+
237+
{:ok, _repo} = Workspace.ensure_repository(token, repo_owner, repo_name)
238+
{:ok, owner} = Workspace.ensure_user(token, repo_owner)
239+
240+
Repo.transact(fn repo ->
241+
{:ok, org} =
242+
repo.insert(%User{
243+
type: :organization,
244+
id: Nanoid.generate(),
245+
display_name: owner.name,
246+
avatar_url: owner.avatar_url,
247+
last_context: "repo/#{repo_owner}/#{repo_name}"
248+
})
249+
250+
{:ok, user} =
251+
repo.insert(%User{
252+
type: :individual,
253+
id: Nanoid.generate(),
254+
last_context: "preview/#{org.id}/#{repo_owner}/#{repo_name}"
255+
})
256+
257+
{:ok, %{org: org, user: user}}
258+
end)
259+
end
231260
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
defmodule AlgoraWeb.OrgPreviewCallbackController do
2+
use AlgoraWeb, :controller
3+
4+
import Ecto.Query
5+
6+
alias Algora.Accounts.User
7+
alias Algora.Repo
8+
9+
require Logger
10+
11+
def new(conn, %{"id" => id, "token" => token} = params) do
12+
with {:ok, _login_token} <- AlgoraWeb.UserAuth.verify_preview_code(token, id),
13+
{:ok, user} <-
14+
Repo.fetch_one(
15+
from u in User,
16+
where: u.id == ^id,
17+
where: is_nil(u.handle),
18+
where: is_nil(u.provider_login)
19+
) do
20+
conn =
21+
if params["return_to"] do
22+
put_session(conn, :user_return_to, String.trim_leading(params["return_to"], AlgoraWeb.Endpoint.url()))
23+
else
24+
conn
25+
end
26+
27+
conn
28+
|> put_flash(:info, "Welcome to Algora!")
29+
|> AlgoraWeb.UserAuth.log_in_user(user)
30+
else
31+
{:error, reason} ->
32+
Logger.debug("failed preview exchange #{inspect(reason)}")
33+
34+
conn
35+
|> put_flash(:error, "Something went wrong. Please try again.")
36+
|> redirect(to: "/")
37+
end
38+
end
39+
end

lib/algora_web/controllers/user_auth.ex

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@ defmodule AlgoraWeb.UserAuth do
224224
defp maybe_store_return_to(conn), do: conn
225225

226226
def signed_in_path_from_context("personal"), do: ~p"/home"
227+
228+
def signed_in_path_from_context("preview/" <> ctx) do
229+
case String.split(ctx, "/") do
230+
[_id, repo_owner, repo_name] -> ~p"/go/#{repo_owner}/#{repo_name}"
231+
_ -> ~p"/home"
232+
end
233+
end
234+
227235
def signed_in_path_from_context(org_handle), do: ~p"/org/#{org_handle}"
228236

229237
def signed_in_path(%User{} = user) do
@@ -293,6 +301,28 @@ defmodule AlgoraWeb.UserAuth do
293301
end
294302
end
295303

304+
def sign_preview_code(payload) do
305+
Phoenix.Token.sign(AlgoraWeb.Endpoint, login_code_salt(), payload, max_age: login_code_ttl())
306+
end
307+
308+
def verify_preview_code(code, id) do
309+
case Phoenix.Token.verify(AlgoraWeb.Endpoint, login_code_salt(), code, max_age: login_code_ttl()) do
310+
{:ok, token_id} ->
311+
if token_id == id do
312+
{:ok, token_id}
313+
else
314+
{:error, :invalid_id}
315+
end
316+
317+
{:error, reason} ->
318+
{:error, reason}
319+
end
320+
end
321+
322+
def preview_path(id, token), do: ~p"/preview?id=#{id}&token=#{token}"
323+
324+
def preview_path(id, token, return_to), do: ~p"/preview?id=#{id}&token=#{token}&return_to=#{return_to}"
325+
296326
def login_path(email, token), do: ~p"/callbacks/email/oauth?email=#{email}&token=#{token}"
297327

298328
def login_path(email, token, return_to),

lib/algora_web/live/org/preview_nav.ex

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,36 @@
11
defmodule AlgoraWeb.Org.PreviewNav do
22
@moduledoc false
33
use Phoenix.Component
4+
use AlgoraWeb, :verified_routes
45

56
import Phoenix.LiveView
67

7-
alias Algora.Accounts.User
8-
alias Algora.Github.TokenPool
9-
alias Algora.Workspace
8+
alias Algora.Organizations
109

1110
def on_mount(:default, %{"repo_owner" => repo_owner, "repo_name" => repo_name}, _session, socket) do
12-
token = TokenPool.get_token()
13-
{:ok, _repo} = Workspace.ensure_repository(token, repo_owner, repo_name)
14-
{:ok, user} = Workspace.ensure_user(token, repo_owner)
15-
16-
current_org = %User{
17-
id: Ecto.UUID.generate(),
18-
provider: "github",
19-
provider_login: repo_owner,
20-
name: user.name,
21-
handle: user.handle,
22-
avatar_url: user.avatar_url
23-
}
24-
25-
{:cont,
26-
socket
27-
|> assign(:current_context, current_org)
28-
|> assign(:all_contexts, [current_org])
29-
|> assign(:new_bounty_form, to_form(%{"github_issue_url" => "", "amount" => ""}))
30-
|> assign(:current_org, current_org)
31-
|> assign(:current_user_role, :admin)
32-
|> assign(:nav, nav_items(repo_owner, repo_name))
33-
|> assign(:contacts, [])
34-
|> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3)}
11+
current_context = socket.assigns[:current_context]
12+
13+
if current_context && current_context.last_context == "repo/#{repo_owner}/#{repo_name}" do
14+
{:cont,
15+
socket
16+
|> assign(:new_bounty_form, to_form(%{"github_issue_url" => "", "amount" => ""}))
17+
|> assign(:current_org, current_context)
18+
|> assign(:current_user_role, :admin)
19+
|> assign(:nav, nav_items(repo_owner, repo_name))
20+
|> assign(:contacts, [])
21+
|> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3)}
22+
else
23+
case Organizations.init_preview(repo_owner, repo_name) do
24+
{:ok, %{user: user, org: _org}} ->
25+
token = AlgoraWeb.UserAuth.sign_preview_code(user.id)
26+
path = AlgoraWeb.UserAuth.preview_path(user.id, token, ~p"/go/#{repo_owner}/#{repo_name}")
27+
28+
{:halt, redirect(socket, to: path)}
29+
30+
{:error, reason} ->
31+
{:cont, put_flash(socket, :error, "Failed to initialize preview: #{inspect(reason)}")}
32+
end
33+
end
3534
end
3635

3736
defp handle_active_tab_params(_params, _url, socket) do

lib/algora_web/router.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ defmodule AlgoraWeb.Router do
6363
get "/a/:table_prefix/:activity_id", ActivityController, :get
6464
get "/auth/logout", OAuthCallbackController, :sign_out
6565
get "/tip", TipController, :create
66+
get "/preview", OrgPreviewCallbackController, :new
6667

6768
scope "/callbacks" do
6869
get "/stripe/refresh", StripeCallbackController, :refresh

test/algora/organizations_test.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,18 @@ defmodule Algora.OrganizationsTest do
187187
assert result3.org.handle == "piedpiperhq"
188188
end
189189
end
190+
191+
describe "init_preview/1" do
192+
test "creates a new user and org if they don't exist" do
193+
assert {:ok, %{user: user, org: org}} = Algora.Organizations.init_preview("acme", "repo")
194+
195+
assert is_nil(org.handle)
196+
assert org.type == :organization
197+
assert org.last_context == "repo/acme/repo"
198+
199+
assert is_nil(user.handle)
200+
assert user.type == :individual
201+
assert user.last_context == "preview/#{org.id}/acme/repo"
202+
end
203+
end
190204
end

test/algora_web/controllers/user_auth_test.exs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,47 @@ defmodule AlgoraWeb.UserAuthTest do
8181
assert {:error, :invalid} = UserAuth.verify_login_code("", "[email protected]")
8282
end
8383
end
84+
85+
describe "verify_preview_code/2" do
86+
test "successfully verifies simple id token" do
87+
id = "123"
88+
code = UserAuth.sign_preview_code(id)
89+
90+
assert {:ok, result} = UserAuth.verify_preview_code(code, id)
91+
assert result == id
92+
end
93+
94+
test "rejects invalid id" do
95+
id = "123"
96+
code = UserAuth.sign_preview_code(id)
97+
98+
assert {:error, :invalid_id} = UserAuth.verify_preview_code(code, "wrong")
99+
end
100+
101+
test "rejects tampered tokens" do
102+
code = "tampered.token.here"
103+
assert {:error, :invalid} = UserAuth.verify_preview_code(code, "123")
104+
end
105+
106+
test "rejects expired tokens" do
107+
id = "123"
108+
original_config = Application.get_env(:algora, :login_code)
109+
Application.put_env(:algora, :login_code, Keyword.put(original_config, :ttl, 1))
110+
111+
code = UserAuth.sign_preview_code(id)
112+
Process.sleep(1500)
113+
114+
assert {:error, :expired} = UserAuth.verify_preview_code(code, id)
115+
116+
Application.put_env(:algora, :login_code, original_config)
117+
end
118+
119+
test "handles nil input" do
120+
assert {:error, :missing} = UserAuth.verify_preview_code(nil, "123")
121+
end
122+
123+
test "handles empty string input" do
124+
assert {:error, :invalid} = UserAuth.verify_preview_code("", "123")
125+
end
126+
end
84127
end

0 commit comments

Comments
 (0)