Merge pull request #269 from asciinema/token-login

Recorder token based login
travis-rake-and-mix
Marcin Kulik 7 years ago committed by GitHub
commit 3247fffd73

@ -1,7 +1,7 @@
class CurrentUserDecorator < UserDecorator
def display_name
model.username || model.email
model.username || model.email || model.temporary_username || "Me"
end
end

@ -4,6 +4,7 @@ use Mix.Config
# you can enable the server option below.
config :asciinema, Asciinema.Endpoint,
http: [port: 4001],
secret_key_base: "ssecretkeybasesecretkeybasesecretkeybasesecretkeybaseecretkeybase",
server: false
# Print only warnings and errors during test

@ -24,7 +24,7 @@ server {
client_max_body_size 16m;
location ~ ^/(phoenix/|css/|js/|images/|fonts/|docs/?|api/asciicasts) {
location ~ ^/(phoenix/|css/|js/|images/|fonts/|docs/?|api/asciicasts|/connect/) {
try_files /maintenance.html $uri/index.html $uri.html $uri @phoenix;
}

@ -8,12 +8,21 @@ defmodule Asciinema.Auth do
opts
end
def call(%Plug.Conn{assigns: %{current_user: %User{}}} = conn, _opts) do
conn
end
def call(conn, _opts) do
user_id = get_session(conn, @user_key)
user = user_id && Repo.get(User, user_id)
assign(conn, :current_user, user)
end
def login(conn, %User{id: id} = user) do
conn
|> put_session(@user_key, id)
|> assign(:current_user, user)
end
def get_basic_auth(conn) do
with ["Basic " <> auth] <- get_req_header(conn, "authorization"),
auth = String.replace(auth, ~r/^%/, ""), # workaround for 1.3.0-1.4.0 client bug

@ -1,5 +1,6 @@
defmodule Asciinema.Users do
import Ecto.Query, warn: false
import Ecto, only: [assoc: 2]
alias Asciinema.{Repo, User, ApiToken}
def authenticate(api_token) do
@ -55,4 +56,17 @@ defmodule Asciinema.Users do
|> ApiToken.revoke_changeset
|> Repo.update!
end
def merge!(dst_user, src_user) do
Repo.transaction(fn ->
asciicasts_q = from(assoc(src_user, :asciicasts))
Repo.update_all(asciicasts_q, set: [user_id: dst_user.id, updated_at: Timex.now])
api_tokens_q = from(assoc(src_user, :api_tokens))
Repo.update_all(api_tokens_q, set: [user_id: dst_user.id, updated_at: Timex.now])
expiring_tokens_q = from(assoc(src_user, :expiring_tokens))
Repo.delete_all(expiring_tokens_q)
Repo.delete!(src_user)
dst_user
end)
end
end

@ -0,0 +1,104 @@
defmodule Asciinema.SessionControllerTest do
use Asciinema.ConnCase
alias Asciinema.{Users, User, ApiToken}
@revoked_token "eb927b31-9ca3-4a6a-8a0c-dfba318e2e84"
@regular_user_token "c4ecd96a-9a16-464d-be6a-bc1f3c50c4ae"
@other_regular_user_token "b26c2fe0-603b-4b10-b0fa-f6ec85628831"
@tmp_user_token "863f6ae5-3f32-4ffc-8d47-284222d6225f"
@other_tmp_user_token "2eafaa20-80c8-47fc-b014-74072027edae"
setup %{conn: conn} do
%User{} = Users.get_user_with_api_token("revoked", @revoked_token)
@revoked_token |> Users.get_api_token! |> Users.revoke_api_token!
regular_user = fixture(:user)
ApiToken.create_changeset(regular_user, @regular_user_token) |> Repo.insert!
other_regular_user = fixture(:user, %{username: "other", email: "other@example.com"})
ApiToken.create_changeset(other_regular_user, @other_regular_user_token) |> Repo.insert!
%User{} = tmp_user = Users.get_user_with_api_token("tmp", @tmp_user_token)
%User{} = Users.get_user_with_api_token("other_tmp", @other_tmp_user_token)
{:ok, conn: conn, regular_user: regular_user, tmp_user: tmp_user}
end
test "invalid token", %{conn: conn} do
conn = get conn, "/connect/nopenope"
assert redirected_to(conn, 302) == "/"
assert get_rails_flash(conn, :alert) =~ ~r/invalid token/i
end
test "revoked token", %{conn: conn} do
conn = get conn, "/connect/#{@revoked_token}"
assert redirected_to(conn, 302) == "/"
assert get_rails_flash(conn, :alert) =~ ~r/been revoked/i
end
test "guest with tmp user token", %{conn: conn} do
conn = get conn, "/connect/#{@tmp_user_token}"
assert redirected_to(conn, 302) == "/user/edit"
assert get_rails_flash(conn, :notice) =~ ~r/welcome.+username.+email/i
end
test "guest with regular user token", %{conn: conn} do
conn = get conn, "/connect/#{@regular_user_token}"
assert redirected_to(conn, 302) == "/~test"
assert get_rails_flash(conn, :notice) =~ ~r/welcome back/i
end
test "tmp user with his own token", %{conn: conn, tmp_user: user} do
conn = login_as(conn, user)
conn = get conn, "/connect/#{@tmp_user_token}"
assert redirected_to(conn, 302) == "/user/edit"
assert get_rails_flash(conn, :notice)
end
test "tmp user with other tmp user token", %{conn: conn, tmp_user: user} do
conn = login_as(conn, user)
conn = get conn, "/connect/#{@other_tmp_user_token}"
assert redirected_to(conn, 302) == "/user/edit"
assert get_rails_flash(conn, :notice)
end
test "tmp user with other regular user token", %{conn: conn, tmp_user: user} do
conn = login_as(conn, user)
conn = get conn, "/connect/#{@regular_user_token}"
assert redirected_to(conn, 302) == "/~test"
assert get_rails_flash(conn, :notice)
end
test "regular user with other tmp user token", %{conn: conn, regular_user: user} do
conn = login_as(conn, user)
conn = get conn, "/connect/#{@tmp_user_token}"
assert redirected_to(conn, 302) == "/~test"
assert get_rails_flash(conn, :notice)
end
test "regular user with his own token", %{conn: conn, regular_user: user} do
conn = login_as(conn, user)
conn = get conn, "/connect/#{@regular_user_token}"
assert redirected_to(conn, 302) == "/~test"
assert get_rails_flash(conn, :notice)
end
test "regular user with other regular user token", %{conn: conn, regular_user: user} do
conn = login_as(conn, user)
conn = get conn, "/connect/#{@other_regular_user_token}"
assert redirected_to(conn, 302) == "/~test"
assert get_rails_flash(conn, :alert)
end
defp get_rails_flash(conn, key) do
conn
|> get_session(:flash)
|> get_in([:flashes, key])
end
defp login_as(conn, user) do
assign(conn, :current_user, user)
end
end

@ -11,9 +11,10 @@ defmodule Asciinema.Fixtures do
content_type: "application/json"}
end
def fixture(:user, _attrs) do
attrs = %{username: "test",
auth_token: "authy-auth-auth"}
def fixture(:user, attrs) do
attrs = Map.merge(%{username: "test",
email: "test@example.com",
auth_token: "authy-auth-auth"}, attrs)
Repo.insert!(User.changeset(%User{}, attrs))
end

@ -0,0 +1,78 @@
defmodule Asciinema.SessionController do
use Asciinema.Web, :controller
import Asciinema.UserView, only: [profile_path: 1]
alias Asciinema.{Auth, Users, User}
def create(conn, %{"api_token" => api_token}) do
case Users.authenticate(api_token) do
{:ok, user} ->
login(conn, user)
{:error, :token_not_found} ->
conn
|> put_rails_flash(:alert, "Invalid token. Make sure you pasted the URL correctly.")
|> redirect(to: "/")
{:error, :token_revoked} ->
conn
|> put_rails_flash(:alert, "This token has been revoked.")
|> redirect(to: "/")
end
end
defp login(conn, logging_user) do
current_user = conn.assigns.current_user
case {current_user, logging_user} do
{nil, %User{email: nil}} ->
conn
|> Auth.login(logging_user)
|> put_rails_flash(:notice, "Welcome! Setting username and email will help you with logging in later.")
|> redirect_to_edit_profile
{nil, %User{}} ->
conn
|> Auth.login(logging_user)
|> put_rails_flash(:notice, "Welcome back!")
|> redirect_to_profile
{%User{id: id, email: nil}, %User{id: id}} ->
conn
|> put_rails_flash(:notice, "Setting username and email will help you with logging in later.")
|> redirect_to_edit_profile
{%User{email: nil}, %User{email: nil}} ->
Users.merge!(current_user, logging_user)
conn
|> put_rails_flash(:notice, "Setting username and email will help you with logging in later.")
|> redirect_to_edit_profile
{%User{email: nil}, %User{}} ->
Users.merge!(logging_user, current_user)
conn
|> Auth.login(logging_user)
|> put_rails_flash(:notice, "Recorder token has been added to your account.")
|> redirect_to_profile
{%User{}, %User{email: nil}} ->
Users.merge!(current_user, logging_user)
conn
|> put_rails_flash(:notice, "Recorder token has been added to your account.")
|> redirect_to_profile
{%User{id: id}, %User{id: id}} ->
conn
|> put_rails_flash(:notice, "You're already logged in.")
|> redirect_to_profile
{%User{}, %User{}} ->
conn
|> put_rails_flash(:alert, "This recorder token belongs to a different user.")
|> redirect_to_profile
# TODO offer merging
end
end
defp put_rails_flash(conn, key, value) do
put_session(conn, :flash, %{discard: [], flashes: %{key => value}})
end
defp redirect_to_profile(conn) do
redirect(conn, to: profile_path(conn.assigns.current_user))
end
defp redirect_to_edit_profile(conn) do
redirect(conn, to: user_path(conn, :edit))
end
end

@ -0,0 +1,7 @@
defmodule Asciinema.ExpiringToken do
use Asciinema.Web, :model
schema "expiring_tokens" do
belongs_to :user, Asciinema.User
end
end

@ -14,6 +14,8 @@ defmodule Asciinema.User do
timestamps(inserted_at: :created_at)
has_many :asciicasts, Asciinema.Asciicast
has_many :api_tokens, Asciinema.ApiToken
has_many :expiring_tokens, Asciinema.ExpiringToken
end
@doc """

@ -50,6 +50,8 @@ defmodule Asciinema.Router do
get "/docs", DocController, :index
get "/docs/:topic", DocController, :show
get "/connect/:api_token", SessionController, :create
end
scope "/api", Asciinema.Api, as: :api do
@ -65,6 +67,10 @@ end
defmodule Asciinema.Router.Helpers.Extra do
alias Asciinema.Router.Helpers, as: H
def user_path(_conn, :edit) do
"/user/edit"
end
def asciicast_file_download_path(conn, asciicast) do
H.asciicast_file_path(conn, :show, asciicast)
|> String.replace_suffix("/json", ".json")

Loading…
Cancel
Save