diff --git a/.formatter.exs b/.formatter.exs index aeba3486..544be4a4 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,5 +1,5 @@ [ - inputs: ["mix.exs", ".formatter.exs", ".credo.exs", "{config,lib,test,priv}/**/*.{ex,exs}"], + inputs: ["mix.exs", ".formatter.exs", ".credo.exs", "{config,lib,test,rel,priv}/**/*.{ex,exs}"], import_deps: [:ecto, :phoenix], line_length: 180 ] diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 79daf1b6..5cb73ef8 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -12,26 +12,26 @@ The goal of the Code of Conduct is to specify a baseline standard of behavior so These are the values Accent developers should aspire to: - * Be friendly and welcoming - * Be patient - * Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.) - * Be thoughtful - * Productive communication requires effort. Think about how your words will be interpreted. - * Remember that sometimes it is best to refrain entirely from commenting. - * Be respectful - * In particular, respect differences of opinion. It is important that we resolve disagreements and differing views constructively. - * Avoid destructive behavior - * Derailing: stay on topic; if you want to talk about something else, start a new conversation. - * Unconstructive criticism: don't merely decry the current state of affairs; offer (or at least solicit) suggestions as to how things may be improved. - * Snarking (pithy, unproductive, sniping comments). +- Be friendly and welcoming +- Be patient + - Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.) +- Be thoughtful + - Productive communication requires effort. Think about how your words will be interpreted. + - Remember that sometimes it is best to refrain entirely from commenting. +- Be respectful + - In particular, respect differences of opinion. It is important that we resolve disagreements and differing views constructively. +- Avoid destructive behavior + - Derailing: stay on topic; if you want to talk about something else, start a new conversation. + - Unconstructive criticism: don't merely decry the current state of affairs; offer (or at least solicit) suggestions as to how things may be improved. + - Snarking (pithy, unproductive, sniping comments). The following actions are explicitly forbidden: - * Insulting, demeaning, hateful, or threatening remarks. - * Discrimination based on age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic. - * Bullying or systematic harassment. - * Unwelcome sexual advances. - * Incitement to any of these. +- Insulting, demeaning, hateful, or threatening remarks. +- Discrimination based on age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic. +- Bullying or systematic harassment. +- Unwelcome sexual advances. +- Incitement to any of these. ## Acknowledgements diff --git a/README.md b/README.md index 2fc3c44b..d5725024 100644 --- a/README.md +++ b/README.md @@ -14,23 +14,23 @@ Accent provides a powerful abstraction around the process maintaining translations in a web/native app. -* **History**. Full history control and actions rollback. _Who_ did _what_, _when_. -* **UI**. Simple yet powerful UI to enable translator and developer to be productive. -* **CLI**. [Command line tool](https://github.com/mirego/accent-cli) to easily add Accent to your developer flow. -* **Collaboration**. Centralize your discussions around translations. -* **GraphQL**. The API that powers the UI is open and documented. It’s easy to build a plugin/cli/library around Accent. +- **History**. Full history control and actions rollback. _Who_ did _what_, _when_. +- **UI**. Simple yet powerful UI to enable translator and developer to be productive. +- **CLI**. [Command line tool](https://github.com/mirego/accent-cli) to easily add Accent to your developer flow. +- **Collaboration**. Centralize your discussions around translations. +- **GraphQL**. The API that powers the UI is open and documented. It’s easy to build a plugin/cli/library around Accent. ## Contents -| Section | Description | -|---------------------------------------------------------|---------------------------------------------------------------------------| -| [🚧 Requirements](#-requirements) | Dependencies required to run Accent’ stack | -| [🎛 Mix commands](#-executing-mix-commands) | How to execute mix task with the Twelve-Factor pattern | -| [🏎 Quickstart](#-quickstart) | Steps to run the project, from API to webapp, with or without Docker | -| [🌳 Environment variables](#-environment-variables) | Required and optional env var used | -| [✅ Tests](#-tests) | How to run the extensive tests suite | -| [🚀 Heroku](#-heroku) | Easy deployment setup with Heroku | -| [🌎 Contribute](#-contribute) | How to contribute to this repo | +| Section | Description | +| --------------------------------------------------- | -------------------------------------------------------------------- | +| [🚧 Requirements](#-requirements) | Dependencies required to run Accent’ stack | +| [🎛 Mix commands](#-executing-mix-commands) | How to execute mix task with the Twelve-Factor pattern | +| [🏎 Quickstart](#-quickstart) | Steps to run the project, from API to webapp, with or without Docker | +| [🌳 Environment variables](#-environment-variables) | Required and optional env var used | +| [✅ Tests](#-tests) | How to run the extensive tests suite | +| [🚀 Heroku](#-heroku) | Easy deployment setup with Heroku | +| [🌎 Contribute](#-contribute) | How to contribute to this repo | ## 🚧 Requirements @@ -71,7 +71,7 @@ _This is the full development setup. To simply run the app, see the *Docker* ins 7. Start Phoenix endpoint with `mix phx.server` 8. Start Ember server with `npm run start --prefix webapp` -*That’s it!* +_That’s it!_ ### Makefile @@ -87,43 +87,58 @@ _When running the production env, you need to provide a valid GOOGLE_API_CLIENT_ 2. Run `make dev-start-postgresql` to start an instance of Postgresql. The instance will run on port 5432 with the `postgres` user. You can change those values in the `docker-compose.yml` file. 3. Run `make dev-start-application` to start the app! The release hook of the release will execute migrations and seeds before starting the webserver on port 4000 (again you can change the settings in `docker-compose.yml`) -*That’s it! You now have a working Accent instance without installing Elixir or Node!* +_That’s it! You now have a working Accent instance without installing Elixir or Node!_ ## 🌳 Environment variables Accent provides a default value for every required environment variable. This means that with the right PostgreSQL setup, you can just run `mix phx.server`. -| Variable | Default | Description | -|---------------------|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `MIX_ENV` | `dev` | The application environment (`dev`, `prod`, or `test`) | -| `DATABASE_URL` | `postgres://localhost/accent_development` | A valid database URL | -| `CANONICAL_HOST` | `localhost` | The host that will be used to build internal URLs | -| `PORT` | `4000` | A port to run the API on | -| `WEBAPP_PORT` | `4200` | A port to run the Webapp on (only used in `dev` environment) | -| `API_HOST` | `http://localhost:4000` | The API host | -| `API_WS_HOST` | `ws://localhost:4000` | The API Websocket host | +| Variable | Default | Description | +| ---------------- | ----------------------------------------- | ------------------------------------------------------------------------------------- | +| `MIX_ENV` | `dev` | The application environment (`dev`, `prod`, or `test`) | +| `DATABASE_URL` | `postgres://localhost/accent_development` | A valid database URL | +| `CANONICAL_HOST` | `localhost` | The host that will be used to build internal URLs | +| `PORT` | `4000` | A port to run the API on | +| `WEBAPP_PORT` | `4200` | A port to run the Webapp on (only used in `dev` environment) | +| `API_HOST` | `http://localhost:4000` | The API host | +| `API_WS_HOST` | `ws://localhost:4000` | The API Websocket host | +| `WEBAPP_URL` | `http://localhost:4000` | The Web client’s endpoint. Used in the authentication process and in the sent emails. | + ### Production setup -| Variable | Default | Description | -|------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `SENTRY_DSN` | _none_ | The *secret* Sentry DSN used to collect API runtime errors | -| `WEBAPP_SENTRY_DSN` | _none_ | The *public* Sentry DSN used to collect Webapp runtime errors | -| `GOOGLE_API_CLIENT_ID` | _none_ | When deploying in a `prod` environment, the Google login is the only way to authenticate user. In `dev` environment, a fake login provider is used so you don’t have to setup a Google app. | -| `RESTRICTED_DOMAIN` | _none_ | If specified, only authenticated users from this domain name will be able to create new projects. | -| `DUMMY_LOGIN_ENABLED` | _none_ | If specified, the password-less authentication (with only the email) will be available. | +| Variable | Default | Description | +| ------------------- | ------- | ------------------------------------------------------------------------------------------------- | +| `SENTRY_DSN` | _none_ | The _secret_ Sentry DSN used to collect API runtime errors | +| `WEBAPP_SENTRY_DSN` | _none_ | The _public_ Sentry DSN used to collect Webapp runtime errors | +| `RESTRICTED_DOMAIN` | _none_ | If specified, only authenticated users from this domain name will be able to create new projects. | + +### Authentication setup + +Various login providers are included in Accent using the awesomer Uberauth library to abstract services. + +| Variable | Default | Description | +| -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `DUMMY_LOGIN_ENABLED` | _none_ | If specified (or no other authentication configs are provided), the password-less authentication (with only the email) will be available. | +| `GOOGLE_API_CLIENT_ID` | _none_ | | +| `GOOGLE_API_CLIENT_SECRET` | _none_ | | +| `GITHUB_CLIENT_ID` | _none_ | | +| `GITHUB_CLIENT_SECRET` | _none_ | | +| `SLACK_CLIENT_ID` | _none_ | | +| `SLACK_CLIENT_SECRET` | _none_ | | +| `SLACK_TEAM_ID` | _none_ | | ### Email setup + If you want to send emails, you’ll have to configure the following environment variables: -| Variable | Default | Description | -| --- | --- | --- | -| `WEBAPP_EMAIL_HOST` | _none_ | The Web client’s hostname. Used in the sent emails to link to the right URL. | -| `MAILER_FROM` | _none_ | The email address used to send emails. | -| `SMTP_ADDRESS` | _none_ | The SMTP server address you want to use to send your emails. | -| `SMTP_PORT` | _none_ | The port ex: (25, 465, 587). | -| `SMTP_USERNAME` | _none_ | The username for authentification. | -| `SMTP_PASSWORD` | _none_ | The password for authentification. | -| `SMTP_API_HEADER` | _none_ | An optional API header that will be added to sent emails. | +| Variable | Default | Description | +| ----------------- | ------- | ------------------------------------------------------------ | +| `MAILER_FROM` | _none_ | The email address used to send emails. | +| `SMTP_ADDRESS` | _none_ | The SMTP server address you want to use to send your emails. | +| `SMTP_PORT` | _none_ | The port ex: (25, 465, 587). | +| `SMTP_USERNAME` | _none_ | The username for authentification. | +| `SMTP_PASSWORD` | _none_ | The password for authentification. | +| `SMTP_API_HEADER` | _none_ | An optional API header that will be added to sent emails. | ## ✅ Tests @@ -157,16 +172,16 @@ Don’t forget to run the `./priv/scripts/ci-check.sh` script to make sure that ## Contributors -* [@simonprev](https://github.com/simonprev) -* [@loboulet](https://github.com/loboulet) -* [@remiprev](https://github.com/remiprev) -* [@charlesdemers](https://github.com/charlesdemers) -* [@ddrmanxbxfr](https://github.com/ddrmanxbxfr) -* [@thermech](https://github.com/thermech) +- [@simonprev](https://github.com/simonprev) +- [@loboulet](https://github.com/loboulet) +- [@remiprev](https://github.com/remiprev) +- [@charlesdemers](https://github.com/charlesdemers) +- [@ddrmanxbxfr](https://github.com/ddrmanxbxfr) +- [@thermech](https://github.com/thermech) ## License -Accent is © 2015-2018 [Mirego](https://www.mirego.com) and may be freely distributed under the [New BSD license](http://opensource.org/licenses/BSD-3-Clause). See the [`LICENSE.md`](https://github.com/mirego/accent/blob/master/LICENSE.md) file. +Accent is © 2015-2019 [Mirego](https://www.mirego.com) and may be freely distributed under the [New BSD license](http://opensource.org/licenses/BSD-3-Clause). See the [`LICENSE.md`](https://github.com/mirego/accent/blob/master/LICENSE.md) file. ## About Mirego diff --git a/config/prod.exs b/config/prod.exs index e4f7f2fa..bb3b9c98 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -6,6 +6,4 @@ config :accent, Accent.Endpoint, root: ".", cache_static_manifest: "priv/static/cache_manifest.json" -config :accent, dummy_provider_enabled: System.get_env("DUMMY_LOGIN_ENABLED") - config :logger, level: :info diff --git a/config/test.exs b/config/test.exs index 0c602e52..68d298c0 100644 --- a/config/test.exs +++ b/config/test.exs @@ -17,7 +17,7 @@ config :accent, Accent.Repo, pool: Ecto.Adapters.SQL.Sandbox config :accent, Accent.Mailer, - webapp_host: "http://example.com", + webapp_url: "http://example.com", mailer_from: "accent-test@example.com", x_smtpapi_header: ~s({"category": ["test", "accent-api-test"]}), adapter: Bamboo.TestAdapter diff --git a/lib/accent/auth/uberauth/dummy_strategy.ex b/lib/accent/auth/uberauth/dummy_strategy.ex new file mode 100644 index 00000000..78a1b849 --- /dev/null +++ b/lib/accent/auth/uberauth/dummy_strategy.ex @@ -0,0 +1,47 @@ +defmodule Accent.Auth.Ueberauth.DummyStrategy do + @moduledoc """ + A username strategy for Ueberauth + """ + + use Ueberauth.Strategy + + alias Ueberauth.Auth.Credentials + alias Ueberauth.Auth.Extra + alias Ueberauth.Auth.Info + + @doc """ + iex> uid(%Plug.Conn{params: %{"email" => "foo"}}) + "foo" + """ + @impl Ueberauth.Strategy + def uid(conn) do + conn.params["email"] + end + + @doc """ + iex> info(%Plug.Conn{params: %{"email" => "foo"}}) + %Ueberauth.Auth.Info{email: "foo"} + """ + @impl Ueberauth.Strategy + def info(conn) do + %Info{email: conn.params["email"]} + end + + @doc """ + iex> credentials(%Plug.Conn{}) + %Ueberauth.Auth.Credentials{} + """ + @impl Ueberauth.Strategy + def credentials(_conn) do + %Credentials{} + end + + @doc """ + iex> extra(%Plug.Conn{params: %{"email" => "foo"}}) + %Ueberauth.Auth.Extra{raw_info: %{"email" => "foo"}} + """ + @impl Ueberauth.Strategy + def extra(conn) do + %Extra{raw_info: conn.params} + end +end diff --git a/lib/accent/auth/user_remote/adapter/fetcher.ex b/lib/accent/auth/user_remote/adapter/fetcher.ex deleted file mode 100644 index 21845011..00000000 --- a/lib/accent/auth/user_remote/adapter/fetcher.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Accent.UserRemote.Adapter.Fetcher do - alias Accent.UserRemote.Adapter.User - - @callback fetch(String.t()) :: {:ok, User.t()} | {:error, list(String.t())} -end diff --git a/lib/accent/auth/user_remote/adapters/dummy.ex b/lib/accent/auth/user_remote/adapters/dummy.ex deleted file mode 100644 index cff90fcb..00000000 --- a/lib/accent/auth/user_remote/adapters/dummy.ex +++ /dev/null @@ -1,17 +0,0 @@ -if Application.get_env(:accent, :dummy_provider_enabled) do - defmodule Accent.UserRemote.Adapters.Dummy do - @moduledoc """ - This is the simplest adapter for user remote fetching. - - It simply returns the value as both the email and the uid. - """ - - @behaviour Accent.UserRemote.Adapter.Fetcher - @name "dummy" - - alias Accent.UserRemote.Adapter.User - - def fetch(value) when value === "", do: {:error, ["invalid email"]} - def fetch(value), do: {:ok, %User{email: String.downcase(value), provider: @name, uid: value}} - end -end diff --git a/lib/accent/auth/user_remote/adapters/google.ex b/lib/accent/auth/user_remote/adapters/google.ex deleted file mode 100644 index 5bcceea3..00000000 --- a/lib/accent/auth/user_remote/adapters/google.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Accent.UserRemote.Adapters.Google do - @moduledoc """ - Fetches the email and the uid from the id_token - using the Google API v3 token info endpoint. - """ - - @behaviour Accent.UserRemote.Adapter.Fetcher - @name "google" - - alias Accent.UserRemote.Adapter.User - - defmodule TokenInfoClient do - use HTTPoison.Base - - @base_url "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=" - - def process_url(token), do: @base_url <> token - - def process_response_body(body), do: Jason.decode!(body) - end - - def fetch(token) when token === "", do: {:error, "invalid token"} - - def fetch(token) do - token - |> TokenInfoClient.get() - |> case do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - email = String.downcase(body["email"]) - {:ok, %User{provider: @name, fullname: body["name"], picture_url: body["picture"], email: email, uid: email}} - - {:ok, %HTTPoison.Response{status_code: 400}} -> - {:error, "invalid token"} - - {:error, %HTTPoison.Error{reason: reason}} -> - {:error, reason} - end - end -end diff --git a/lib/accent/auth/user_remote/authenticator.ex b/lib/accent/auth/user_remote/authenticator.ex index 5daa92a3..c815d466 100644 --- a/lib/accent/auth/user_remote/authenticator.ex +++ b/lib/accent/auth/user_remote/authenticator.ex @@ -1,27 +1,34 @@ defmodule Accent.UserRemote.Authenticator do - alias Accent.UserRemote.{CollaboratorNormalizer, Fetcher, Persister, TokenGiver} + alias Accent.UserRemote.{CollaboratorNormalizer, Persister, TokenGiver, User} - def authenticate(provider, uid) do - provider - |> fetch(uid) - |> persist - |> normalize_collaborators - |> grant_token + def authenticate(%{provider: provider, info: info}) do + info + |> map_user(provider) + |> Persister.persist() + |> CollaboratorNormalizer.normalize() + |> TokenGiver.grant_token() end - defp fetch(provider, uid), do: Fetcher.fetch(provider, uid) - - defp persist({:error, error}), do: {:error, error} - defp persist({:ok, user}), do: Persister.persist(user) - - defp normalize_collaborators({:error, error}), do: {:error, error} - - defp normalize_collaborators({:ok, user, provider}) do - CollaboratorNormalizer.normalize(user) - - {:ok, user, provider} + defp map_user(info, :dummy) do + %User{ + provider: "dummy", + fullname: info.email, + email: normalize_email(info.email), + uid: normalize_email(info.email) + } end - defp grant_token({:error, error}), do: {:error, error} - defp grant_token({:ok, user, _provider}), do: TokenGiver.grant_token(user) + defp map_user(info, provider) do + %User{ + provider: to_string(provider), + fullname: info.name, + picture_url: info.image, + email: normalize_email(info.email), + uid: normalize_email(info.email) + } + end + + defp normalize_email(email) do + String.downcase(email) + end end diff --git a/lib/accent/auth/user_remote/collaborator_normalizer.ex b/lib/accent/auth/user_remote/collaborator_normalizer.ex index 40cc2826..c9dfd4e0 100644 --- a/lib/accent/auth/user_remote/collaborator_normalizer.ex +++ b/lib/accent/auth/user_remote/collaborator_normalizer.ex @@ -3,20 +3,21 @@ defmodule Accent.UserRemote.CollaboratorNormalizer do alias Accent.{Collaborator, Repo, User} - @spec normalize(User.t()) :: :ok - def normalize(%User{id: id, email: email}) do + @spec normalize(User.t()) :: User.t() + def normalize(user = %User{id: id, email: email}) do email |> fetch_collaborators() |> assign_user_id(id) |> Enum.each(&Repo.update/1) - :ok + user end defp fetch_collaborators(email) do email = String.downcase(email) - from(c in Collaborator, where: [email: ^email]) + Collaborator + |> from(where: [email: ^email]) |> Repo.all() end diff --git a/lib/accent/auth/user_remote/fetcher.ex b/lib/accent/auth/user_remote/fetcher.ex deleted file mode 100644 index 8925f326..00000000 --- a/lib/accent/auth/user_remote/fetcher.ex +++ /dev/null @@ -1,21 +0,0 @@ -defmodule Accent.UserRemote.Fetcher do - @moduledoc """ - Fetch a user (email and provider infos) based on a single value. - """ - - alias Accent.UserRemote.Adapters.Google - - def fetch(_provider, ""), do: {:error, %{value: "empty"}} - def fetch(_provider, nil), do: {:error, %{value: "empty"}} - - def fetch("dummy", value) do - if Application.get_env(:accent, :dummy_provider_enabled) do - Accent.UserRemote.Adapters.Dummy.fetch(value) - else - fetch(nil, value) - end - end - - def fetch("google", value), do: Google.fetch(value) - def fetch(_provider, _value), do: {:error, %{provider: "unknown"}} -end diff --git a/lib/accent/auth/user_remote/persister.ex b/lib/accent/auth/user_remote/persister.ex index e628b8d6..dd8a1a3e 100644 --- a/lib/accent/auth/user_remote/persister.ex +++ b/lib/accent/auth/user_remote/persister.ex @@ -15,28 +15,31 @@ defmodule Accent.UserRemote.Persister do alias Accent.AuthProvider alias Accent.Repo alias Accent.User, as: RepoUser - alias Accent.UserRemote.Adapter.User, as: FetchedUser + alias Accent.UserRemote.User, as: FetchedUser alias Ecto.Changeset - @spec persist(FetchedUser.t()) :: {:ok, RepoUser.t(), AuthProvider.t()} + @spec persist(FetchedUser.t()) :: RepoUser.t() def persist(user = %FetchedUser{provider: provider, uid: uid}) do - user = find_user(user) - provider = find_provider(user, provider, uid) - - {:ok, user, provider} + user + |> find_or_create_user() + |> find_or_create_provider(provider, uid) end - defp find_user(fetched_user) do + defp find_or_create_user(fetched_user) do case Repo.get_by(RepoUser, email: fetched_user.email) do - user = %RepoUser{} -> update_user(user, fetched_user) - _ -> create_user(fetched_user) + nil -> create_user(fetched_user) + user -> update_user(user, fetched_user) end end - defp find_provider(user, provider_name, uid) do + defp find_or_create_provider(user, provider_name, uid) do case Repo.get_by(AuthProvider, name: provider_name, uid: uid) do - provider = %AuthProvider{} -> provider - _ -> create_provider(user, provider_name, uid) + nil -> + create_provider(user, provider_name, uid) + user + + _ -> + user end end diff --git a/lib/accent/auth/user_remote/token_giver.ex b/lib/accent/auth/user_remote/token_giver.ex index 67ab9264..1244c705 100644 --- a/lib/accent/auth/user_remote/token_giver.ex +++ b/lib/accent/auth/user_remote/token_giver.ex @@ -4,10 +4,7 @@ defmodule Accent.UserRemote.TokenGiver do def grant_token(user) do invalidate_tokens(user) - - token = create_token(user) - - {:ok, user, token} + create_token(user) end defp invalidate_tokens(user) do @@ -17,9 +14,9 @@ defmodule Accent.UserRemote.TokenGiver do end defp create_token(user) do - user - |> Ecto.build_assoc(:access_tokens) - |> Map.put(:token, SecureRandom.urlsafe_base64(70)) - |> Repo.insert!() + token = Ecto.build_assoc(user, :access_tokens) + token = %{token | token: SecureRandom.urlsafe_base64(70)} + + Repo.insert(token) end end diff --git a/lib/accent/auth/user_remote/adapter/user.ex b/lib/accent/auth/user_remote/user.ex similarity index 81% rename from lib/accent/auth/user_remote/adapter/user.ex rename to lib/accent/auth/user_remote/user.ex index 174251a8..6871dd51 100644 --- a/lib/accent/auth/user_remote/adapter/user.ex +++ b/lib/accent/auth/user_remote/user.ex @@ -1,4 +1,4 @@ -defmodule Accent.UserRemote.Adapter.User do +defmodule Accent.UserRemote.User do defstruct ~w(email provider uid fullname picture_url)a @type t :: %__MODULE__{email: String.t(), provider: String.t(), uid: String.t(), fullname: String.t(), picture_url: String.t()} diff --git a/lib/accent/projects/project_creator.ex b/lib/accent/projects/project_creator.ex index 40fd1f17..22df0306 100644 --- a/lib/accent/projects/project_creator.ex +++ b/lib/accent/projects/project_creator.ex @@ -43,11 +43,9 @@ defmodule Accent.ProjectCreator do end def generate_bot_user_with_access do - {:ok, bot_user, _token} = - @bot - |> Repo.insert!() - |> TokenGiver.grant_token() + user = Repo.insert!(@bot) + {:ok, _token} = TokenGiver.grant_token(user) - bot_user + user end end diff --git a/lib/graphql/types/viewer.ex b/lib/graphql/types/viewer.ex index 931d1760..9876eca7 100644 --- a/lib/graphql/types/viewer.ex +++ b/lib/graphql/types/viewer.ex @@ -4,7 +4,7 @@ defmodule Accent.GraphQL.Types.Viewer do import Accent.GraphQL.Helpers.Authorization object :viewer do - field(:user, :user) + field(:user, :user, resolve: fn user, _, _ -> {:ok, user} end) field :permissions, list_of(:string) do resolve(viewer_authorize(:index_permissions, &Accent.GraphQL.Resolvers.Permission.list_viewer/3)) diff --git a/lib/web/controllers/auth_controller.ex b/lib/web/controllers/auth_controller.ex new file mode 100644 index 00000000..6f4112d8 --- /dev/null +++ b/lib/web/controllers/auth_controller.ex @@ -0,0 +1,25 @@ +defmodule Accent.AuthController do + use Phoenix.Controller + + alias Accent.UserRemote.Authenticator + + plug Ueberauth + + def callback(conn = %{assigns: %{ueberauth_auth: auth}}, _) do + case Authenticator.authenticate(auth) do + {:ok, token} -> + redirect(conn, external: webapp_url() <> "?token=" <> token.token) + + _ -> + redirect(conn, external: webapp_url()) + end + end + + def callback(conn, _) do + redirect(conn, external: webapp_url()) + end + + defp webapp_url do + Application.get_env(:accent, :webapp_url) + end +end diff --git a/lib/web/controllers/authentication_controller.ex b/lib/web/controllers/authentication_controller.ex deleted file mode 100644 index 58d55220..00000000 --- a/lib/web/controllers/authentication_controller.ex +++ /dev/null @@ -1,43 +0,0 @@ -defmodule Accent.AuthenticationController do - use Plug.Builder - - alias Accent.UserRemote.Authenticator - - import Phoenix.Controller, only: [json: 2] - - plug(:fetch_authentication) - plug(:create) - - def create(conn = %{assigns: %{user: user, token: token}}, _) do - conn - |> json(%{ - token: token.token, - user: %{ - id: user.id, - email: user.email, - picture_url: user.picture_url, - fullname: user.fullname - } - }) - end - - def create(conn, _) do - conn - |> put_status(:unauthorized) - |> json(%{error: conn.assigns[:error]}) - end - - defp fetch_authentication(conn = %{params: %{"uid" => uid, "provider" => provider}}, _) do - case Authenticator.authenticate(provider, uid) do - {:ok, user, token} -> - conn - |> assign(:user, user) - |> assign(:token, token) - - {:error, error} -> - assign(conn, :error, error) - end - end - - defp fetch_authentication(conn, _), do: assign(conn, :error, "Invalid params") -end diff --git a/lib/web/router.ex b/lib/web/router.ex index 9ce0ac44..8cdca291 100644 --- a/lib/web/router.ex +++ b/lib/web/router.ex @@ -51,7 +51,7 @@ defmodule Accent.Router do scope "/", Accent do # Users - post("/auth", AuthenticationController, :create) + # post("/auth", AuthenticationController, :create) get("/:id/percentage_reviewed_badge.svg", BadgeController, :percentage_reviewed_count) get("/:id/reviewed_badge.svg", BadgeController, :reviewed_count) @@ -59,6 +59,13 @@ defmodule Accent.Router do get("/:id/translations_badge.svg", BadgeController, :translations_count) end + scope "/auth", Accent do + pipe_through [:browser] + + get "/:provider", AuthController, :request + get "/:provider/callback", AuthController, :callback + end + scope "/", Accent do pipe_through(:browser) diff --git a/lib/web/templates/email/create_comment.html.eex b/lib/web/templates/email/create_comment.html.eex index 627e2c56..968a9df8 100644 --- a/lib/web/templates/email/create_comment.html.eex +++ b/lib/web/templates/email/create_comment.html.eex @@ -1,4 +1,4 @@

"><%= @commenter.email %> has commented on <%= @translation.key %>:

"><%= @comment.text %>

-

">You can reply here

+

">You can reply here

diff --git a/lib/web/templates/email/create_comment.text.eex b/lib/web/templates/email/create_comment.text.eex index 6a8efc23..c8df4adc 100644 --- a/lib/web/templates/email/create_comment.text.eex +++ b/lib/web/templates/email/create_comment.text.eex @@ -3,4 +3,4 @@ --- <%= @comment.text %> -You can reply here (<%= webapp_host() %><%= @translation_path %>) +You can reply here (<%= webapp_url() %><%= @translation_path %>) diff --git a/lib/web/templates/email/project_invite.html.eex b/lib/web/templates/email/project_invite.html.eex index 86b7bcd3..a90b217a 100644 --- a/lib/web/templates/email/project_invite.html.eex +++ b/lib/web/templates/email/project_invite.html.eex @@ -1,5 +1,5 @@

"><%= @user.email %> has invited you to collaborate on the project "<%= @project.name %>"

-

">If you already have an account (<%= @email %>), you can just login and you’ll see the project.

+

">If you already have an account (<%= @email %>), you can just login and you’ll see the project.

-

">If not, creating an account is as simple as logging in with the invited Google account (<%= @email %>) on <%= webapp_host() %>

+

">If not, creating an account is as simple as logging in with the invited Google account (<%= @email %>) on <%= webapp_url() %>

diff --git a/lib/web/templates/email/project_invite.text.eex b/lib/web/templates/email/project_invite.text.eex index 5d370b84..1d4871b2 100644 --- a/lib/web/templates/email/project_invite.text.eex +++ b/lib/web/templates/email/project_invite.text.eex @@ -1,5 +1,5 @@ <%= @user.email %> has invited you to collaborate on the project "<%= @project.name %>". -If you already have an account (<%= @email %>), you can just login (<%= webapp_host() %>) and you’ll see the project. +If you already have an account (<%= @email %>), you can just login (<%= webapp_url() %>) and you’ll see the project. -If not, creating an account is as simple as logging in with the invited Google account (<%= @email %>) on <%= webapp_host() %> +If not, creating an account is as simple as logging in with the invited Google account (<%= @email %>) on <%= webapp_url() %> diff --git a/lib/web/templates/email_layout/index.html.eex b/lib/web/templates/email_layout/index.html.eex index 06501115..94665726 100644 --- a/lib/web/templates/email_layout/index.html.eex +++ b/lib/web/templates/email_layout/index.html.eex @@ -13,7 +13,7 @@
- " href="<%= webapp_host() %>"><%= webapp_host() %> + " href="<%= webapp_url() %>"><%= webapp_url() %> diff --git a/lib/web/templates/email_layout/index.text.eex b/lib/web/templates/email_layout/index.text.eex index 209954e1..9c193500 100644 --- a/lib/web/templates/email_layout/index.text.eex +++ b/lib/web/templates/email_layout/index.text.eex @@ -6,4 +6,4 @@ Accent - The Accent Team -(<%= webapp_host() %>) +(<%= webapp_url() %>) diff --git a/lib/web/views/email_view_config_helper.ex b/lib/web/views/email_view_config_helper.ex index ed8ff673..510a2266 100644 --- a/lib/web/views/email_view_config_helper.ex +++ b/lib/web/views/email_view_config_helper.ex @@ -1,7 +1,7 @@ defmodule Accent.EmailViewConfigHelper do def x_smtpapi_header, do: config()[:x_smtpapi_header] def mailer_from, do: config()[:mailer_from] - def webapp_host, do: config()[:webapp_host] + def webapp_url, do: config()[:webapp_url] defp config do Application.get_env(:accent, Accent.Mailer) diff --git a/mix.exs b/mix.exs index 67dd47ed..7ef7e214 100644 --- a/mix.exs +++ b/mix.exs @@ -73,6 +73,13 @@ defmodule Accent.Mixfile do {:xml_builder, "~> 2.0"}, {:ex_minimatch, "~> 0.0.1"}, + # Auth + {:oauth2, "~> 0.9", override: true}, + {:ueberauth, "~> 0.6"}, + {:ueberauth_google, "~> 0.6"}, + {:ueberauth_github, "~> 0.7"}, + {:ueberauth_slack, github: "ueberauth/ueberauth_slack", ref: "525594c870f959ab"}, + # Errors {:sentry, "~> 7.0"}, diff --git a/mix.lock b/mix.lock index 6f8410f8..f6e8519a 100644 --- a/mix.lock +++ b/mix.lock @@ -51,6 +51,7 @@ "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.3.2", "e98e998fd76c191c7e1a9557c8617912c53df3d4a6132f561eb762b699ef59fa", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mox": {:hex, :mox, "0.5.0", "c519b48407017a85f03407a9a4c4ceb7cc6dec5fe886b2241869fb2f08476f9e", [:mix], [], "hexpm"}, + "oauth2": {:hex, :oauth2, "0.9.4", "632e8e8826a45e33ac2ea5ac66dcc019ba6bb5a0d2ba77e342d33e3b7b252c6e", [:mix], [{:hackney, "~> 1.7", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "p1_utils": {:git, "https://github.com/processone/p1_utils.git", "bcd456d11908fd6087235d1aef941a8db5f25ea3", []}, "parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, @@ -73,6 +74,10 @@ "sentry": {:hex, :sentry, "7.0.3", "093fa4b6937760afb9a5fcb0e4a9092a305b6c0ff26a710e977614b201feab75", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, + "ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "ueberauth_github": {:hex, :ueberauth_github, "0.7.0", "637067c5500f7b13c18caca3db66d09eba661524e0d0e9518b54151e99484bad", [:mix], [{:oauth2, "~> 0.9", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.4", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm"}, + "ueberauth_google": {:hex, :ueberauth_google, "0.8.0", "dc0e8417061c74107a3ba1419943cc930d3403b5c536b3757886964a3a70c333", [:mix], [{:oauth2, "~> 0.9", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.4", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm"}, + "ueberauth_slack": {:git, "https://github.com/ueberauth/ueberauth_slack.git", "525594c870f959aba67acc759d5c1a588ee75e9e", [ref: "525594c870f959ab"]}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"}, "xml_builder": {:hex, :xml_builder, "2.1.1", "2d6d665f09cf1319e3e1c46035755271b414d99ad8615d0bd6f337623e0c885b", [:mix], [], "hexpm"}, diff --git a/package.json b/package.json index 3a85c5ca..8cff497f 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "The new new Accent Web app", "private": true, "scripts": { - "prettier": "prettier --write './{webapp,jipt,cli}/!(node_modules)/**/*.{js,ts,json,gql,svg}'", - "prettier-check": "prettier --check './{webapp,jipt,cli}/!(node_modules)/**/*.{js,ts,json,gql,svg}'", + "prettier": "prettier --write './{webapp,jipt,cli}/!(node_modules)/**/*.{js,ts,json,gql,svg,md}' '*.md'", + "prettier-check": "prettier --check './{webapp,jipt,cli}/!(node_modules)/**/*.{js,ts,json,gql,svg,md}' '*.md'", "lint-scripts": "eslint webapp/. cli/. jipt/.", "lint-scripts-fix": "eslint --fix webapp/.", "tslint-scripts": "tslint -c tslint.json '{cli,jipt}/src/**/*.{js,ts,json}'", diff --git a/rel/config/config.exs b/rel/config/config.exs index 1d9369ca..0cd6fe7a 100644 --- a/rel/config/config.exs +++ b/rel/config/config.exs @@ -28,10 +28,10 @@ config :accent, Accent.Repo, url: System.get_env("DATABASE_URL") || "postgres://localhost/accent_development" config :accent, + webapp_url: System.get_env("WEBAPP_URL") || "http://localhost:4000", force_ssl: Utilities.string_to_boolean(System.get_env("FORCE_SSL")), hook_broadcaster: Accent.Hook.Broadcaster, hook_github_file_server: Accent.Hook.Consumers.GitHub.FileServer.HTTP, - dummy_provider_enabled: true, restricted_domain: System.get_env("RESTRICTED_DOMAIN") # Configures canary custom handlers and repo @@ -40,6 +40,26 @@ config :canary, unauthorized_handler: {Accent.ErrorController, :handle_unauthorized}, not_found_handler: {Accent.ErrorController, :handle_not_found} +providers = [] +providers = if System.get_env("GOOGLE_API_CLIENT_ID"), do: [{:google, {Ueberauth.Strategy.Google, [scope: "email openid"]}} | providers], else: providers +providers = if System.get_env("SLACK_CLIENT_ID"), do: [{:slack, {Ueberauth.Strategy.Slack, [team: System.get_env("SLACK_TEAM_ID")]}} | providers], else: providers +providers = if System.get_env("GITHUB_CLIENT_ID"), do: [{:github, {Ueberauth.Strategy.Github, []}} | providers], else: providers +providers = if System.get_env("DUMMY_LOGIN_ENABLED") || providers === [], do: [{:dummy, {Accent.Auth.Ueberauth.DummyStrategy, []}} | providers], else: providers + +config :ueberauth, Ueberauth, providers: providers + +config :ueberauth, Ueberauth.Strategy.Google.OAuth, + client_id: System.get_env("GOOGLE_API_CLIENT_ID"), + client_secret: System.get_env("GOOGLE_API_CLIENT_SECRET") + +config :ueberauth, Ueberauth.Strategy.Github.OAuth, + client_id: System.get_env("GITHUB_CLIENT_ID"), + client_secret: System.get_env("GITHUB_CLIENT_SECRET") + +config :ueberauth, Ueberauth.Strategy.Slack.OAuth, + client_id: System.get_env("SLACK_CLIENT_ID"), + client_secret: System.get_env("SLACK_CLIENT_SECRET") + # Configures Elixir's Logger config :logger, :console, format: "$time $metadata[$level] $message\n", diff --git a/rel/config/mailer.exs b/rel/config/mailer.exs index dc4ecb54..918a24da 100644 --- a/rel/config/mailer.exs +++ b/rel/config/mailer.exs @@ -2,7 +2,7 @@ use Mix.Config if System.get_env("SMTP_ADDRESS") do config :accent, Accent.Mailer, - webapp_host: System.get_env("WEBAPP_EMAIL_HOST"), + webapp_url: System.get_env("WEBAPP_URL"), mailer_from: System.get_env("MAILER_FROM"), adapter: Bamboo.SMTPAdapter, server: System.get_env("SMTP_ADDRESS"), @@ -12,7 +12,7 @@ if System.get_env("SMTP_ADDRESS") do x_smtpapi_header: System.get_env("SMTP_API_HEADER") else config :accent, Accent.Mailer, - webapp_host: System.get_env("WEBAPP_EMAIL_HOST"), + webapp_url: System.get_env("WEBAPP_URL"), mailer_from: System.get_env("MAILER_FROM"), adapter: Bamboo.LocalAdapter end diff --git a/test/auth/uberauth/dummy_strategy_test.exs b/test/auth/uberauth/dummy_strategy_test.exs new file mode 100644 index 00000000..084c2f29 --- /dev/null +++ b/test/auth/uberauth/dummy_strategy_test.exs @@ -0,0 +1,4 @@ +defmodule AccentTest.Auth.Uberauth.DummyStrategy do + use ExUnit.Case, async: true + doctest(Accent.Auth.Ueberauth.DummyStrategy, import: true) +end diff --git a/test/auth/user_remote/adapters/google_test.exs b/test/auth/user_remote/adapters/google_test.exs deleted file mode 100644 index f8ac588d..00000000 --- a/test/auth/user_remote/adapters/google_test.exs +++ /dev/null @@ -1,55 +0,0 @@ -defmodule AccentTest.UserRemote.Adapters.Google do - use Accent.RepoCase, async: false - - import Mock - - alias Accent.UserRemote.Adapter.User - alias Accent.UserRemote.Adapters.Google - - defp mock_response(status, body) do - %HTTPoison.Response{status_code: status, body: body} - end - - test "valid" do - response = [request: fn _, _, _, _, _, _, _, _, _ -> {:ok, mock_response(200, %{"email" => "test@example.com", "name" => "Test"})} end] - - with_mock HTTPoison.Base, response do - expected_user = %User{email: "test@example.com", fullname: "Test", picture_url: nil, provider: "google", uid: "test@example.com"} - assert Google.fetch("test") == {:ok, expected_user} - end - end - - test "valid with picture" do - response = [request: fn _, _, _, _, _, _, _, _, _ -> {:ok, mock_response(200, %{"email" => "test@example.com", "name" => "Test", "picture" => "test.jpg"})} end] - - with_mock HTTPoison.Base, response do - expected_user = %User{email: "test@example.com", fullname: "Test", picture_url: "test.jpg", provider: "google", uid: "test@example.com"} - assert Google.fetch("test") == {:ok, expected_user} - end - end - - test "valid with uppercase email" do - response = [request: fn _, _, _, _, _, _, _, _, _ -> {:ok, mock_response(200, %{"email" => "TEST@example.com", "name" => "Test"})} end] - - with_mock HTTPoison.Base, response do - expected_user = %User{email: "test@example.com", fullname: "Test", picture_url: nil, provider: "google", uid: "test@example.com"} - assert Google.fetch("test") == {:ok, expected_user} - end - end - - test "invalid with status" do - response = [request: fn _, _, _, _, _, _, _, _, _ -> {:ok, mock_response(400, %{})} end] - - with_mock HTTPoison.Base, response do - assert Google.fetch("test") == {:error, "invalid token"} - end - end - - test "error" do - response = [request: fn _, _, _, _, _, _, _, _, _ -> {:error, %HTTPoison.Error{reason: "no internet"}} end] - - with_mock HTTPoison.Base, response do - assert Google.fetch("test") == {:error, "no internet"} - end - end -end diff --git a/test/auth/user_remote/authenticator_test.exs b/test/auth/user_remote/authenticator_test.exs index 0bf45fa5..d34507d2 100644 --- a/test/auth/user_remote/authenticator_test.exs +++ b/test/auth/user_remote/authenticator_test.exs @@ -12,17 +12,18 @@ defmodule AccentTest.UserRemote.Authenticator do } test "grant token new user" do - {:ok, user, token} = Authenticator.authenticate("dummy", "test@example.com") + {:ok, token} = Authenticator.authenticate(%{provider: :dummy, info: %{email: "test@example.com"}}) + user = Repo.get_by(User, email: "test@example.com") assert user.email == "test@example.com" assert token.user_id == user.id end test "grant token existing user" do - {:ok, user, _token} = Authenticator.authenticate("dummy", "test@example.com") - {:ok, existing_user, _token} = Authenticator.authenticate("dummy", "test@example.com") + {:ok, _token} = Authenticator.authenticate(%{provider: :dummy, info: %{email: "test@example.com"}}) + {:ok, _token} = Authenticator.authenticate(%{provider: :dummy, info: %{email: "test@example.com"}}) - assert user.id == existing_user.id + assert Repo.get_by(User, email: "test@example.com") end test "normalize collaborators with email" do @@ -31,7 +32,8 @@ defmodule AccentTest.UserRemote.Authenticator do project = %Project{main_color: "#f00", name: "My project", language_id: language.id} |> Repo.insert!() collaborator = %Collaborator{project_id: project.id, role: "admin", assigner_id: assigner.id, email: "test@example.com"} |> Repo.insert!() - {:ok, user, _token} = Authenticator.authenticate("dummy", "test@example.com") + {:ok, _token} = Authenticator.authenticate(%{provider: :dummy, info: %{email: "test@example.com"}}) + user = Repo.get_by(User, email: "test@example.com") updated_collaborator = Repo.get(Collaborator, collaborator.id) assert updated_collaborator.user_id == user.id @@ -43,7 +45,8 @@ defmodule AccentTest.UserRemote.Authenticator do project = %Project{main_color: "#f00", name: "My project", language_id: language.id} |> Repo.insert!() collaborator = %Collaborator{project_id: project.id, role: "admin", assigner_id: assigner.id, email: "test@example.com"} |> Repo.insert!() - {:ok, user, _token} = Authenticator.authenticate("dummy", "TeSt@eXamPle.com") + {:ok, _token} = Authenticator.authenticate(%{provider: :dummy, info: %{email: "TeSt@eXamPle.com"}}) + user = Repo.get_by(User, email: "test@example.com") updated_collaborator = Repo.get(Collaborator, collaborator.id) assert updated_collaborator.user_id == user.id diff --git a/test/auth/user_remote/fetcher_test.exs b/test/auth/user_remote/fetcher_test.exs deleted file mode 100644 index 03139d14..00000000 --- a/test/auth/user_remote/fetcher_test.exs +++ /dev/null @@ -1,38 +0,0 @@ -defmodule AccentTest.UserRemote.Fetcher do - use Accent.RepoCase, async: false - - import Mock - - alias Accent.UserRemote.Adapter.User - alias Accent.UserRemote.Fetcher - - defp mock_response(status, body) do - %HTTPoison.Response{status_code: status, body: body} - end - - test "google" do - response = [request: fn _, _, _, _, _, _, _, _, _ -> {:ok, mock_response(200, %{"email" => "test@example.com", "name" => "Test"})} end] - - with_mock HTTPoison.Base, response do - expected_user = %User{email: "test@example.com", fullname: "Test", picture_url: nil, provider: "google", uid: "test@example.com"} - assert Fetcher.fetch("google", "test") == {:ok, expected_user} - end - end - - test "dummy" do - expected_user = %User{email: "test@example.com", provider: "dummy", uid: "test@example.com"} - assert Fetcher.fetch("dummy", "test@example.com") == {:ok, expected_user} - end - - test "nil token" do - assert Fetcher.fetch("dummy", nil) == {:error, %{value: "empty"}} - end - - test "empty token" do - assert Fetcher.fetch("dummy", "") == {:error, %{value: "empty"}} - end - - test "unknown provider" do - assert Fetcher.fetch("foo", "test") == {:error, %{provider: "unknown"}} - end -end diff --git a/test/auth/user_remote/persister_test.exs b/test/auth/user_remote/persister_test.exs index 5fdcb917..41b9413d 100644 --- a/test/auth/user_remote/persister_test.exs +++ b/test/auth/user_remote/persister_test.exs @@ -4,35 +4,35 @@ defmodule AccentTest.UserRemote.Persister do alias Accent.AuthProvider alias Accent.Repo alias Accent.User - alias Accent.UserRemote.Adapter.User, as: UserFromFetcher alias Accent.UserRemote.Persister + alias Accent.UserRemote.User, as: UserFromFetcher @user %UserFromFetcher{email: "test@test.com", provider: "google", uid: "1234"} test "persist with new user" do - {:ok, user, provider} = Persister.persist(@user) + user = Persister.persist(@user) assert user.id === Repo.get_by!(User, email: "test@test.com").id - assert provider.id === Repo.get_by!(AuthProvider, uid: "1234", name: "google", user_id: user.id).id + assert Repo.get_by!(AuthProvider, uid: "1234", name: "google", user_id: user.id) end test "persist with existing user existing provider" do existing_user = Repo.insert!(%User{email: @user.email}) - existing_provider = Repo.insert!(%AuthProvider{name: @user.provider, uid: @user.uid}) + Repo.insert!(%AuthProvider{name: @user.provider, uid: @user.uid}) - {:ok, user, provider} = Persister.persist(@user) + user = Persister.persist(@user) assert user.id === existing_user.id - assert provider.id === existing_provider.id + assert length(Repo.all(AuthProvider)) === 1 end test "persist with existing user new provider" do existing_user = Repo.insert!(%User{email: @user.email}) Repo.insert!(%AuthProvider{name: "dummy", uid: @user.email}) - {:ok, user, provider} = Persister.persist(@user) + user = Persister.persist(@user) assert user.id === existing_user.id - assert provider.id === Repo.get_by!(AuthProvider, uid: "1234", name: "google", user_id: user.id).id + assert Repo.get_by!(AuthProvider, uid: "1234", name: "google", user_id: user.id) end end diff --git a/test/services/collaborator_normalizer_test.exs b/test/services/collaborator_normalizer_test.exs index 4acbc5a3..8ed89953 100644 --- a/test/services/collaborator_normalizer_test.exs +++ b/test/services/collaborator_normalizer_test.exs @@ -23,7 +23,7 @@ defmodule AccentTest.CollaboratorNormalizer do new_user = %User{email: "test@test.com"} |> Repo.insert!() - :ok = CollaboratorNormalizer.normalize(new_user) + %User{} = CollaboratorNormalizer.normalize(new_user) new_collaborators = Collaborator |> where([c], c.id in ^collaborator_ids) |> Repo.all() @@ -48,7 +48,7 @@ defmodule AccentTest.CollaboratorNormalizer do new_user = %User{email: "Test@test.com"} |> Repo.insert!() - :ok = CollaboratorNormalizer.normalize(new_user) + %User{} = CollaboratorNormalizer.normalize(new_user) new_collaborators = Collaborator |> where([c], c.id in ^collaborator_ids) |> Repo.all() @@ -58,7 +58,7 @@ defmodule AccentTest.CollaboratorNormalizer do test "create without collaborations" do new_user = %User{email: "Test@test.com"} |> Repo.insert!() - :ok = CollaboratorNormalizer.normalize(new_user) + %User{} = CollaboratorNormalizer.normalize(new_user) new_collaborators = Collaborator |> Repo.all() diff --git a/test/web/controllers/auth_controller_test.exs b/test/web/controllers/auth_controller_test.exs new file mode 100644 index 00000000..7f7da3d6 --- /dev/null +++ b/test/web/controllers/auth_controller_test.exs @@ -0,0 +1,34 @@ +defmodule AccentTest.AuthenticationController do + use Accent.ConnCase + + alias Accent.{AccessToken, AuthController, Repo, User} + + test "create responds with error when invalid params", %{conn: conn} do + conn = AuthController.callback(conn, nil) + + assert redirected_to(conn, 302) == "http://localhost:4000" + end + + test "create responds with valid dummy params", %{conn: conn} do + conn = + conn + |> assign(:ueberauth_auth, %{provider: :dummy, info: %{email: "dummy@test.com"}}) + |> AuthController.callback(nil) + + user = Repo.get_by(User, email: "dummy@test.com") + token = Repo.get_by(AccessToken, user_id: user.id) + assert redirected_to(conn, 302) =~ "http://localhost:4000?token=#{token.token}" + end + + test "create responds with valid google params", %{conn: conn} do + conn = + conn + |> assign(:ueberauth_auth, %{provider: :google, info: %{name: "Dummy", email: "dummy@test.com", image: nil}}) + |> AuthController.callback(nil) + + user = Repo.get_by(User, email: "dummy@test.com") + token = Repo.get_by(AccessToken, user_id: user.id) + assert user.fullname === "Dummy" + assert redirected_to(conn, 302) =~ "http://localhost:4000?token=#{token.token}" + end +end diff --git a/test/web/controllers/authentication_controller_test.exs b/test/web/controllers/authentication_controller_test.exs deleted file mode 100644 index 7de6fe59..00000000 --- a/test/web/controllers/authentication_controller_test.exs +++ /dev/null @@ -1,31 +0,0 @@ -defmodule AccentTest.AuthenticationController do - use Accent.ConnCase - - test "create responds with error when invalid params", %{conn: conn} do - response = - conn - |> post(authentication_path(conn, :create)) - |> json_response(401) - - assert response == %{"error" => "Invalid params"} - end - - test "create responds with authenticated user", %{conn: conn} do - response = - conn - |> post(authentication_path(conn, :create), %{uid: "test@example.com", provider: "dummy"}) - |> json_response(200) - - assert get_in(response, ["user", "email"]) == "test@example.com" - assert get_in(response, ["token"]) != nil - end - - test "create responds with error on unkown provider", %{conn: conn} do - response = - conn - |> post(authentication_path(conn, :create), %{uid: "test@example.com", provider: "test"}) - |> json_response(401) - - assert response == %{"error" => %{"provider" => "unknown"}} - end -end diff --git a/webapp/app/locales/en/translations.js b/webapp/app/locales/en/translations.js index 383c4475..0282ad61 100644 --- a/webapp/app/locales/en/translations.js +++ b/webapp/app/locales/en/translations.js @@ -38,11 +38,20 @@ export default { translation_comments_subscriptions: { title: 'Notify on new messages' }, - google_login_form: { - title: 'Google authentication', - subtitle: - 'We use your Google account for authentication only, we will not spam you or access any of your sensitive informations.', - login_button: 'Login with Google' + login_header: { + text: 'Login with any service below,', + subtext: + 'Your email address will be used to identify you uniquely through all services.' + }, + login_footer: { + text: 'Nothing to hide, check the source code:', + link: 'mirego/accent' + }, + login_forms: { + github: 'Login with GitHub →', + google: 'Login with Google →', + slack: 'Login with Slack →', + dummy: 'Enter an email and login →' }, dummy_login_form: { title: 'Fake authentication', diff --git a/webapp/app/pods/application/controller.js b/webapp/app/pods/application/controller.js deleted file mode 100644 index 53934544..00000000 --- a/webapp/app/pods/application/controller.js +++ /dev/null @@ -1,3 +0,0 @@ -import Controller from '@ember/controller'; - -export default Controller.extend(); diff --git a/webapp/app/pods/application/route.js b/webapp/app/pods/application/route.js index cb8d06cf..aac0cfb4 100644 --- a/webapp/app/pods/application/route.js +++ b/webapp/app/pods/application/route.js @@ -9,22 +9,20 @@ export default Route.extend({ beforeModel() { raven.config(config.SENTRY.DSN).install(); - this._tryGoogleLoginAfterRedirect(); + this._tryLoginAfterRedirect(); }, - _tryGoogleLoginAfterRedirect() { - if (!config.GOOGLE_LOGIN_ENABLED) return; - - const match = window.location.href - .substring(window.location.href.indexOf('#') + 1) + _tryLoginAfterRedirect() { + const match = window.location.search + .substring(window.location.search.indexOf('?') + 1) .split('&') - .find(segment => segment.split('=')[0] === 'id_token'); + .find(segment => segment.split('=')[0] === 'token'); const token = match && match.split('=')[1]; if (!token) return; - this.session.login({token, provider: 'google'}).then(data => { - if (data && data.token) this.transitionTo('logged-in.projects'); + this.session.login({token}).then(data => { + if (data && data.user) this.transitionTo('logged-in.projects'); }); } }); diff --git a/webapp/app/pods/components/google-login-form/component.js b/webapp/app/pods/components/google-login-form/component.js deleted file mode 100644 index 89962ece..00000000 --- a/webapp/app/pods/components/google-login-form/component.js +++ /dev/null @@ -1,59 +0,0 @@ -import Component from '@ember/component'; -import config from 'accent-webapp/config/environment'; - -export default Component.extend({ - loginEnabled: false, - - didInsertElement() { - const loginButton = this.element.querySelector('.googleLoginButton'); - - this._loadGoogleScript().then(() => { - if (!window.gapi) return; - this.set('loginEnabled', true); - - window.gapi.load('auth2', () => { - const google = window.gapi.auth2.init({ - client_id: config.GOOGLE_API.CLIENT_ID, // eslint-disable-line camelcase - ux_mode: 'redirect', // eslint-disable-line camelcase - cookiepolicy: 'single_host_origin' - }); - - google.attachClickHandler(loginButton); - }); - }); - }, - - _loadGoogleScript() { - return new Promise((resolve, reject) => { - let script = document.createElement('script'); - const prior = this.element; - - script.async = true; - script.defer = true; - - const onloadHander = (_, isAbort) => { - if ( - isAbort || - !script.readyState || - /loaded|complete/.test(script.readyState) - ) { - script.onload = null; - script.onreadystatechange = null; - script = undefined; - - if (isAbort) { - reject(); - } else { - resolve(); - } - } - }; - - script.onload = onloadHander; - script.onreadystatechange = onloadHander; - - script.src = 'https://apis.google.com/js/api:client.js'; - prior.parentNode.insertBefore(script, prior); - }); - } -}); diff --git a/webapp/app/pods/components/google-login-form/styles.scss b/webapp/app/pods/components/google-login-form/styles.scss index f441e4dc..56297510 100644 --- a/webapp/app/pods/components/google-login-form/styles.scss +++ b/webapp/app/pods/components/google-login-form/styles.scss @@ -1,31 +1,16 @@ & { max-width: 500px; - margin: 100px auto 30px; - padding: 20px; - box-shadow: 0 3px 21px rgba(#000, 0.07); - border: 1px solid #eee; - background: $color-light-background; + margin: 0 auto 30px; text-align: center; } -.title { - margin-bottom: 15px; - font-weight: 700; - font-size: 19px; -} - -.subtitle { - margin: 0 15px 15px; - font-size: 14px; -} - .googleLogo { width: 30px; margin-right: 10px; } .googleLoginButton { - opacity: 0.3; + opacity: 1; margin-top: 10px; padding: 7px 40px 7px 20px; background: #4d90fe; @@ -33,13 +18,9 @@ font-family: arial, sans-serif; font-size: 14px; - &.googleLoginButton--enabled { - opacity: 1; - - &:focus, - &:hover { - background: darken(#4d90fe, 8%); - } + &:focus, + &:hover { + background: darken(#4d90fe, 8%); } } diff --git a/webapp/app/pods/components/google-login-form/template.hbs b/webapp/app/pods/components/google-login-form/template.hbs index a3a20ac9..f42a77d2 100644 --- a/webapp/app/pods/components/google-login-form/template.hbs +++ b/webapp/app/pods/components/google-login-form/template.hbs @@ -1,7 +1,4 @@ -

{{t 'components.google_login_form.title'}}

-

{{t 'components.google_login_form.subtitle'}}

- - + diff --git a/webapp/app/pods/components/login-footer/styles.scss b/webapp/app/pods/components/login-footer/styles.scss new file mode 100644 index 00000000..8b99f956 --- /dev/null +++ b/webapp/app/pods/components/login-footer/styles.scss @@ -0,0 +1,27 @@ +& { + max-width: 500px; + margin: 30px auto 60px; + padding-top: 14px; + border-top: 1px solid #eee; +} + +.text { + font-size: 12px; + color: lighten($color-black, 50%); +} + +.link { + color: $color-green; + text-decoration: none; + + &:focus, + &:hover { + text-decoration: underline; + } +} + +@media (max-width: ($screen-sm)) { + & { + margin-top: 30px; + } +} diff --git a/webapp/app/pods/components/login-footer/template.hbs b/webapp/app/pods/components/login-footer/template.hbs new file mode 100644 index 00000000..8e0bf613 --- /dev/null +++ b/webapp/app/pods/components/login-footer/template.hbs @@ -0,0 +1,6 @@ +

+ {{t 'components.login_footer.text'}} + + {{t 'components.login_footer.link'}} + +

diff --git a/webapp/app/pods/components/login-forms/component.js b/webapp/app/pods/components/login-forms/component.js new file mode 100644 index 00000000..9e0a2418 --- /dev/null +++ b/webapp/app/pods/components/login-forms/component.js @@ -0,0 +1,22 @@ +// Vendor +import Component from '@ember/component'; +import {computed} from '@ember/object'; +import config from 'accent-webapp/config/environment'; + +export default Component.extend({ + username: '', + + googleLoginEnabled: computed(() => config.AUTH_PROVIDERS.includes('google')), + dummyLoginEnabled: computed(() => config.AUTH_PROVIDERS.includes('dummy')), + githubLoginEnabled: computed(() => config.AUTH_PROVIDERS.includes('github')), + slackLoginEnabled: computed(() => config.AUTH_PROVIDERS.includes('slack')), + + googleUrl: computed(() => `${config.API.AUTHENTICATION_PATH}/google`), + githubUrl: computed(() => `${config.API.AUTHENTICATION_PATH}/github`), + slackUrl: computed(() => `${config.API.AUTHENTICATION_PATH}/slack`), + dummyUrl: computed('username', function() { + return `${ + config.API.AUTHENTICATION_PATH + }/dummy/callback?email=${this.username}`; + }) +}); diff --git a/webapp/app/pods/components/login-forms/styles.scss b/webapp/app/pods/components/login-forms/styles.scss new file mode 100644 index 00000000..735f8abb --- /dev/null +++ b/webapp/app/pods/components/login-forms/styles.scss @@ -0,0 +1,82 @@ +& { + max-width: 400px; + margin: 0 auto 30px; +} + +.input { + @extend %textInput; + max-width: 300px; + padding: 10px; + width: 100%; + font-family: $font-primary; + font-size: 13px; + border: 2px solid darken($color-green, 5%); + + &:focus { + border-color: $color-green; + } +} + +.loginButton { + max-width: 300px; + position: relative; + display: inline-flex; + justify-content: center; + width: 100%; + padding: 12px 10px; + margin-bottom: 15px; + text-align: center; + background: #fff; + border: 2px solid #888; + font-size: 13px; + + &.loginButton--google { + border: 2px solid darken(#4d90fe, 5%); + text-shadow: none; + color: #4d90fe; + + &:focus, + &:hover { + background: lighten(#4d90fe, 30%); + } + } + + &.loginButton--dummy { + border: 2px solid darken($color-green, 5%); + background: $color-green; + border-radius: 0 0 4px 4px; + margin-top: -3px; + } + + &.loginButton--github { + text-shadow: none; + color: #000; + + &:focus, + &:hover { + background: #eee; + } + } + + &.loginButton--slack { + border: 2px solid #3f0f3f; + text-shadow: none; + color: #3f0f3f; + + &:focus, + &:hover { + background: lighten(#3f0f3f, 79%); + } + } +} + +.loginButton-logo { + position: absolute; + left: 10px; + top: 8px; + width: 26px; + margin-right: 10px; + color: #fff; + font-size: 26px; + line-height: 1; +} diff --git a/webapp/app/pods/components/login-forms/template.hbs b/webapp/app/pods/components/login-forms/template.hbs new file mode 100644 index 00000000..2496bce1 --- /dev/null +++ b/webapp/app/pods/components/login-forms/template.hbs @@ -0,0 +1,32 @@ +{{login-header}} + +{{#if dummyLoginEnabled}} + {{input value=username class="input"}} + + + {{t 'components.login_forms.dummy'}} + +{{/if}} + +{{#if googleLoginEnabled}} + + + {{t 'components.login_forms.google'}} + +{{/if}} + +{{#if githubLoginEnabled}} + + + {{t 'components.login_forms.github'}} + +{{/if}} + +{{#if slackLoginEnabled}} + + + {{t 'components.login_forms.slack'}} + +{{/if}} + +{{login-footer}} diff --git a/webapp/app/pods/components/login-header/styles.scss b/webapp/app/pods/components/login-header/styles.scss new file mode 100644 index 00000000..81a019a5 --- /dev/null +++ b/webapp/app/pods/components/login-header/styles.scss @@ -0,0 +1,44 @@ +& { + max-width: 500px; + margin: 100px auto 60px; +} + +.title { + display: flex; + align-items: center; + margin-bottom: 45px; +} + +.title-name { + margin-left: 15px; + text-decoration: none; + font-size: 25px; + font-weight: 700; + color: $color-black; +} + +.text { + font-size: 17px; + line-height: 1.3; + color: lighten($color-black, 40%); +} + +.text-emphasis { + display: block; + margin-bottom: 10px; + font-weight: bold; + font-size: 22px; + font-style: normal; + color: $color-black; +} + +.logo { + width: 30px; + height: 30px; +} + +@media (max-width: ($screen-sm)) { + & { + margin-top: 30px; + } +} diff --git a/webapp/app/pods/components/login-header/template.hbs b/webapp/app/pods/components/login-header/template.hbs new file mode 100644 index 00000000..98382468 --- /dev/null +++ b/webapp/app/pods/components/login-header/template.hbs @@ -0,0 +1,12 @@ +

+ {{inline-svg 'assets/logo.svg' class='logo'}} + + + {{t 'general.application_name'}} + +

+ +

+ {{t 'components.login_header.text'}} + {{t 'components.login_header.subtext'}} +

diff --git a/webapp/app/pods/components/projects-header/template.hbs b/webapp/app/pods/components/projects-header/template.hbs index 8a039831..0d45b878 100644 --- a/webapp/app/pods/components/projects-header/template.hbs +++ b/webapp/app/pods/components/projects-header/template.hbs @@ -37,8 +37,8 @@ {{#if session.credentials.user}}
- {{#if session.credentials.user.picture_url}} - + {{#if session.credentials.user.pictureUrl}} + {{/if}} {{session.credentials.user.fullname}} diff --git a/webapp/app/pods/login/controller.js b/webapp/app/pods/login/controller.js index 1db84fbb..a7493e5e 100644 --- a/webapp/app/pods/login/controller.js +++ b/webapp/app/pods/login/controller.js @@ -1,30 +1,10 @@ import {inject as service} from '@ember/service'; -import {computed} from '@ember/object'; import Controller from '@ember/controller'; -import config from 'accent-webapp/config/environment'; export default Controller.extend({ jipt: service('jipt'), - session: service('session'), - - username: '', init() { this.jipt.login(); - }, - - googleLoginEnabled: computed(() => config.GOOGLE_LOGIN_ENABLED), - dummyLoginEnabled: computed(() => config.DUMMY_LOGIN_ENABLED), - - actions: { - dummyLogin(token) { - this._login({token, provider: 'dummy'}); - } - }, - - _login({token, provider}) { - this.session.login({token, provider}).then(data => { - if (data && data.token) this.transitionToRoute('logged-in.projects'); - }); } }); diff --git a/webapp/app/pods/login/template.hbs b/webapp/app/pods/login/template.hbs index 210a9e81..975bb74b 100644 --- a/webapp/app/pods/login/template.hbs +++ b/webapp/app/pods/login/template.hbs @@ -1,13 +1,3 @@ -
- {{projects-header session=session}} - - {{#if googleLoginEnabled}} - {{google-login-form}} - {{/if}} - - {{#if dummyLoginEnabled}} - {{dummy-login-form onDummyLogin=(action 'dummyLogin')}} - {{/if}} -
+{{login-forms}} {{application-footer}} diff --git a/webapp/app/services/session.js b/webapp/app/services/session.js index 61bcfb7a..8ddb050f 100644 --- a/webapp/app/services/session.js +++ b/webapp/app/services/session.js @@ -24,19 +24,18 @@ export default Service.extend({ } }), - login(...args) { - return this.sessionCreator - .createSession(...args) - .then(credentials => this.set('credentials', credentials)) - .then(credentials => { - if (credentials && credentials.token) this.jipt.loggedIn(); + login({token}) { + return this.sessionCreator.createSession({token}).then(credentials => { + if (!credentials || !credentials.viewer) return; - return credentials; - }); + this.set('credentials', {token, ...credentials.viewer}); + this.jipt.loggedIn(); + + return credentials.viewer; + }); }, logout() { this.sessionDestroyer.destroySession(); - if (this.googleAuth) this.googleAuth.disconnect(); } }); diff --git a/webapp/app/services/session/creator.js b/webapp/app/services/session/creator.js index f46a27fc..91c53d68 100644 --- a/webapp/app/services/session/creator.js +++ b/webapp/app/services/session/creator.js @@ -3,20 +3,35 @@ import RSVP from 'rsvp'; import config from 'accent-webapp/config/environment'; import fetch from 'fetch'; +const uri = `${config.API.HOST}/graphql`; + export default Service.extend({ - createSession({token, provider}) { + createSession({token}) { return new RSVP.Promise((resolve, reject) => { - const uid = token; const options = { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` }, - body: JSON.stringify({uid, provider}) + body: JSON.stringify({ + query: ` + query Viewer { + viewer { + user { + id + email + pictureUrl + fullname + } + } + } + ` + }) }; - fetch(config.API.AUTHENTICATION_PATH, options) - .then(data => data.json().then(resolve)) + fetch(uri, options) + .then(data => data.json().then(({data}) => resolve(data))) .catch((_jqXHR, _textStatus, error) => reject(error)); }); } diff --git a/webapp/config/environment.js b/webapp/config/environment.js index 7c1694d8..d0244261 100644 --- a/webapp/config/environment.js +++ b/webapp/config/environment.js @@ -6,6 +6,7 @@ module.exports = function(environment) { const wsHost = process.env.API_WS_HOST || 'ws://localhost:4000'; const host = process.env.API_HOST || 'http://localhost:4000'; + const providers = (process.env.WEBAPP_AUTH_PROVIDERS || 'dummy').split(','); const ENV = { modulePrefix: 'accent-webapp', @@ -41,13 +42,7 @@ module.exports = function(environment) { JIPT_SCRIPT_PATH: `${host}/static/jipt/index.js` }; - ENV.GOOGLE_API = { - CLIENT_ID: process.env.GOOGLE_API_CLIENT_ID - }; - - ENV.GOOGLE_LOGIN_ENABLED = Boolean(ENV.GOOGLE_API.CLIENT_ID); - ENV.DUMMY_LOGIN_ENABLED = - Boolean(process.env.DUMMY_LOGIN_ENABLED) || !ENV.GOOGLE_LOGIN_ENABLED; + ENV.AUTH_PROVIDERS = providers; ENV.SENTRY = { DSN: process.env.WEBAPP_SENTRY_DSN diff --git a/webapp/public/assets/auth_providers/dummy.svg b/webapp/public/assets/auth_providers/dummy.svg new file mode 100644 index 00000000..93597016 --- /dev/null +++ b/webapp/public/assets/auth_providers/dummy.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/webapp/public/assets/auth_providers/github.svg b/webapp/public/assets/auth_providers/github.svg new file mode 100644 index 00000000..28217563 --- /dev/null +++ b/webapp/public/assets/auth_providers/github.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/webapp/public/assets/auth_providers/google.svg b/webapp/public/assets/auth_providers/google.svg new file mode 100644 index 00000000..25c56f61 --- /dev/null +++ b/webapp/public/assets/auth_providers/google.svg @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/webapp/public/assets/auth_providers/slack.svg b/webapp/public/assets/auth_providers/slack.svg new file mode 100644 index 00000000..9063858d --- /dev/null +++ b/webapp/public/assets/auth_providers/slack.svg @@ -0,0 +1,22 @@ + + + + + + diff --git a/webapp/public/assets/google-logo.png b/webapp/public/assets/google-logo.png deleted file mode 100644 index 64471f28..00000000 Binary files a/webapp/public/assets/google-logo.png and /dev/null differ