mirror of
https://github.com/mirego/accent.git
synced 2024-07-14 22:30:32 +03:00
Add uberauth to handle authentication to allow multiple providers login (google, github and slack)
This commit is contained in:
parent
d8f927e8f4
commit
768a2fac3e
@ -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
|
||||
]
|
||||
|
@ -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
|
||||
|
||||
|
111
README.md
111
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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
47
lib/accent/auth/uberauth/dummy_strategy.ex
Normal file
47
lib/accent/auth/uberauth/dummy_strategy.ex
Normal file
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()}
|
@ -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
|
||||
|
@ -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))
|
||||
|
25
lib/web/controllers/auth_controller.ex
Normal file
25
lib/web/controllers/auth_controller.ex
Normal file
@ -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
|
@ -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
|
@ -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)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<p style="<%= paragraph_style("font-size": "14px", color: "#444") %>"><%= @commenter.email %> has commented on <code><%= @translation.key %><code>:</p>
|
||||
<p style="<%= paragraph_style("font-size": "18px", color: "#222", "border-left": "3px solid #ddd", "padding-left": "10px") %>"><%= @comment.text %></p>
|
||||
|
||||
<p style="<%= paragraph_style("font-size": "15px") %>">You can reply <a style="<%= link_style() %>" href="<%= webapp_host() %><%= @translation_path %>">here</a></p>
|
||||
<p style="<%= paragraph_style("font-size": "15px") %>">You can reply <a style="<%= link_style() %>" href="<%= webapp_url() %><%= @translation_path %>">here</a></p>
|
||||
|
@ -3,4 +3,4 @@
|
||||
---
|
||||
<%= @comment.text %>
|
||||
|
||||
You can reply here (<%= webapp_host() %><%= @translation_path %>)
|
||||
You can reply here (<%= webapp_url() %><%= @translation_path %>)
|
||||
|
@ -1,5 +1,5 @@
|
||||
<p style="<%= paragraph_style("font-size": "17px", color: "#444") %>"><%= @user.email %> has invited you to collaborate on the project "<%= @project.name %>"</p>
|
||||
|
||||
<p style="<%= paragraph_style("font-size": "15px") %>">If you already have an account (<%= @email %>), you can just <a style="<%= link_style() %>" href="<%= webapp_host() %>">login</a> and you’ll see the project.</p>
|
||||
<p style="<%= paragraph_style("font-size": "15px") %>">If you already have an account (<%= @email %>), you can just <a style="<%= link_style() %>" href="<%= webapp_url() %>">login</a> and you’ll see the project.</p>
|
||||
|
||||
<p style="<%= paragraph_style("font-size": "15px") %>">If not, creating an account is as simple as logging in with the invited Google account (<%= @email %>) on <a style="<%= link_style() %>" href="<%= webapp_host() %>"><%= webapp_host() %></a></p>
|
||||
<p style="<%= paragraph_style("font-size": "15px") %>">If not, creating an account is as simple as logging in with the invited Google account (<%= @email %>) on <a style="<%= link_style() %>" href="<%= webapp_url() %>"><%= webapp_url() %></a></p>
|
||||
|
@ -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() %>
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<br />
|
||||
|
||||
<a style="<%= link_style("font-size": "13px", "text-decoration": "underline") %>" href="<%= webapp_host() %>"><%= webapp_host() %></a>
|
||||
<a style="<%= link_style("font-size": "13px", "text-decoration": "underline") %>" href="<%= webapp_url() %>"><%= webapp_url() %></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -6,4 +6,4 @@ Accent
|
||||
-
|
||||
The Accent Team
|
||||
|
||||
(<%= webapp_host() %>)
|
||||
(<%= webapp_url() %>)
|
||||
|
@ -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)
|
||||
|
7
mix.exs
7
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"},
|
||||
|
||||
|
5
mix.lock
5
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"},
|
||||
|
@ -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}'",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
4
test/auth/uberauth/dummy_strategy_test.exs
Normal file
4
test/auth/uberauth/dummy_strategy_test.exs
Normal file
@ -0,0 +1,4 @@
|
||||
defmodule AccentTest.Auth.Uberauth.DummyStrategy do
|
||||
use ExUnit.Case, async: true
|
||||
doctest(Accent.Auth.Ueberauth.DummyStrategy, import: true)
|
||||
end
|
@ -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
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
34
test/web/controllers/auth_controller_test.exs
Normal file
34
test/web/controllers/auth_controller_test.exs
Normal file
@ -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
|
@ -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
|
@ -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',
|
||||
|
@ -1,3 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default Controller.extend();
|
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
@ -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%);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,4 @@
|
||||
<h1 class="title">{{t 'components.google_login_form.title'}}</h1>
|
||||
<h2 class="subtitle">{{t 'components.google_login_form.subtitle'}}</h2>
|
||||
|
||||
<button class="button button--filled googleLoginButton {{if loginEnabled 'googleLoginButton--enabled'}}">
|
||||
<a href="#" class="button button--filled googleLoginButton">
|
||||
<img src="assets/google-logo.png" class="googleLogo" />
|
||||
{{t 'components.google_login_form.login_button'}}
|
||||
</button>
|
||||
</a>
|
||||
|
27
webapp/app/pods/components/login-footer/styles.scss
Normal file
27
webapp/app/pods/components/login-footer/styles.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
6
webapp/app/pods/components/login-footer/template.hbs
Normal file
6
webapp/app/pods/components/login-footer/template.hbs
Normal file
@ -0,0 +1,6 @@
|
||||
<p class="text">
|
||||
{{t 'components.login_footer.text'}}
|
||||
<a class="link" href="https://github.com/mirego/accent" target="_blank" rel="noopener noreferrer">
|
||||
{{t 'components.login_footer.link'}}
|
||||
</a>
|
||||
</p>
|
22
webapp/app/pods/components/login-forms/component.js
Normal file
22
webapp/app/pods/components/login-forms/component.js
Normal file
@ -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}`;
|
||||
})
|
||||
});
|
82
webapp/app/pods/components/login-forms/styles.scss
Normal file
82
webapp/app/pods/components/login-forms/styles.scss
Normal file
@ -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;
|
||||
}
|
32
webapp/app/pods/components/login-forms/template.hbs
Normal file
32
webapp/app/pods/components/login-forms/template.hbs
Normal file
@ -0,0 +1,32 @@
|
||||
{{login-header}}
|
||||
|
||||
{{#if dummyLoginEnabled}}
|
||||
{{input value=username class="input"}}
|
||||
<a href={{dummyUrl}} class="button button--filled loginButton loginButton--dummy">
|
||||
<img src="assets/auth_providers/dummy.svg" class="loginButton-logo" />
|
||||
{{t 'components.login_forms.dummy'}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if googleLoginEnabled}}
|
||||
<a href={{googleUrl}} class="button button--filled loginButton loginButton--google">
|
||||
<img src="assets/auth_providers/google.svg" class="loginButton-logo" />
|
||||
{{t 'components.login_forms.google'}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if githubLoginEnabled}}
|
||||
<a href={{githubUrl}} class="button button--filled loginButton loginButton--github">
|
||||
<img src="assets/auth_providers/github.svg" class="loginButton-logo" />
|
||||
{{t 'components.login_forms.github'}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if slackLoginEnabled}}
|
||||
<a href={{slackUrl}} class="button button--filled loginButton loginButton--slack">
|
||||
<img src="assets/auth_providers/slack.svg" class="loginButton-logo" />
|
||||
{{t 'components.login_forms.slack'}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{login-footer}}
|
44
webapp/app/pods/components/login-header/styles.scss
Normal file
44
webapp/app/pods/components/login-header/styles.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
12
webapp/app/pods/components/login-header/template.hbs
Normal file
12
webapp/app/pods/components/login-header/template.hbs
Normal file
@ -0,0 +1,12 @@
|
||||
<h1 class="title">
|
||||
{{inline-svg 'assets/logo.svg' class='logo'}}
|
||||
|
||||
<span class="title-name">
|
||||
{{t 'general.application_name'}}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<h2 class="text">
|
||||
<em class="text-emphasis">{{t 'components.login_header.text'}}</em>
|
||||
{{t 'components.login_header.subtext'}}
|
||||
</h2>
|
@ -37,8 +37,8 @@
|
||||
|
||||
{{#if session.credentials.user}}
|
||||
<div class="content-right">
|
||||
{{#if session.credentials.user.picture_url}}
|
||||
<img class="picture" src="{{session.credentials.user.picture_url}}" />
|
||||
{{#if session.credentials.user.pictureUrl}}
|
||||
<img class="picture" src="{{session.credentials.user.pictureUrl}}" />
|
||||
{{/if}}
|
||||
|
||||
<span class="username">{{session.credentials.user.fullname}}</span>
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1,13 +1,3 @@
|
||||
<div>
|
||||
{{projects-header session=session}}
|
||||
|
||||
{{#if googleLoginEnabled}}
|
||||
{{google-login-form}}
|
||||
{{/if}}
|
||||
|
||||
{{#if dummyLoginEnabled}}
|
||||
{{dummy-login-form onDummyLogin=(action 'dummyLogin')}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{login-forms}}
|
||||
|
||||
{{application-footer}}
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
|
16
webapp/public/assets/auth_providers/dummy.svg
Normal file
16
webapp/public/assets/auth_providers/dummy.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="feather feather-alert-circle"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12" y2="16"></line>
|
||||
</svg>
|
After Width: | Height: | Size: 377 B |
14
webapp/public/assets/auth_providers/github.svg
Normal file
14
webapp/public/assets/auth_providers/github.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg
|
||||
viewBox="0 0 256 250"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M128.00106,0 C57.3172926,0 0,57.3066942 0,128.00106 C0,184.555281 36.6761997,232.535542 87.534937,249.460899 C93.9320223,250.645779 96.280588,246.684165 96.280588,243.303333 C96.280588,240.251045 96.1618878,230.167899 96.106777,219.472176 C60.4967585,227.215235 52.9826207,204.369712 52.9826207,204.369712 C47.1599584,189.574598 38.770408,185.640538 38.770408,185.640538 C27.1568785,177.696113 39.6458206,177.859325 39.6458206,177.859325 C52.4993419,178.762293 59.267365,191.04987 59.267365,191.04987 C70.6837675,210.618423 89.2115753,204.961093 96.5158685,201.690482 C97.6647155,193.417512 100.981959,187.77078 104.642583,184.574357 C76.211799,181.33766 46.324819,170.362144 46.324819,121.315702 C46.324819,107.340889 51.3250588,95.9223682 59.5132437,86.9583937 C58.1842268,83.7344152 53.8029229,70.715562 60.7532354,53.0843636 C60.7532354,53.0843636 71.5019501,49.6441813 95.9626412,66.2049595 C106.172967,63.368876 117.123047,61.9465949 128.00106,61.8978432 C138.879073,61.9465949 149.837632,63.368876 160.067033,66.2049595 C184.49805,49.6441813 195.231926,53.0843636 195.231926,53.0843636 C202.199197,70.715562 197.815773,83.7344152 196.486756,86.9583937 C204.694018,95.9223682 209.660343,107.340889 209.660343,121.315702 C209.660343,170.478725 179.716133,181.303747 151.213281,184.472614 C155.80443,188.444828 159.895342,196.234518 159.895342,208.176593 C159.895342,225.303317 159.746968,239.087361 159.746968,243.303333 C159.746968,246.709601 162.05102,250.70089 168.53925,249.443941 C219.370432,232.499507 256,184.536204 256,128.00106 C256,57.3066942 198.691187,0 128.00106,0 Z M47.9405593,182.340212 C47.6586465,182.976105 46.6581745,183.166873 45.7467277,182.730227 C44.8183235,182.312656 44.2968914,181.445722 44.5978808,180.80771 C44.8734344,180.152739 45.876026,179.97045 46.8023103,180.409216 C47.7328342,180.826786 48.2627451,181.702199 47.9405593,182.340212 Z M54.2367892,187.958254 C53.6263318,188.524199 52.4329723,188.261363 51.6232682,187.366874 C50.7860088,186.474504 50.6291553,185.281144 51.2480912,184.70672 C51.8776254,184.140775 53.0349512,184.405731 53.8743302,185.298101 C54.7115892,186.201069 54.8748019,187.38595 54.2367892,187.958254 Z M58.5562413,195.146347 C57.7719732,195.691096 56.4895886,195.180261 55.6968417,194.042013 C54.9125733,192.903764 54.9125733,191.538713 55.713799,190.991845 C56.5086651,190.444977 57.7719732,190.936735 58.5753181,192.066505 C59.3574669,193.22383 59.3574669,194.58888 58.5562413,195.146347 Z M65.8613592,203.471174 C65.1597571,204.244846 63.6654083,204.03712 62.5716717,202.981538 C61.4524999,201.94927 61.1409122,200.484596 61.8446341,199.710926 C62.5547146,198.935137 64.0575422,199.15346 65.1597571,200.200564 C66.2704506,201.230712 66.6095936,202.705984 65.8613592,203.471174 Z M75.3025151,206.281542 C74.9930474,207.284134 73.553809,207.739857 72.1039724,207.313809 C70.6562556,206.875043 69.7087748,205.700761 70.0012857,204.687571 C70.302275,203.678621 71.7478721,203.20382 73.2083069,203.659543 C74.6539041,204.09619 75.6035048,205.261994 75.3025151,206.281542 Z M86.046947,207.473627 C86.0829806,208.529209 84.8535871,209.404622 83.3316829,209.4237 C81.8013,209.457614 80.563428,208.603398 80.5464708,207.564772 C80.5464708,206.498591 81.7483088,205.631657 83.2786917,205.606221 C84.8005962,205.576546 86.046947,206.424403 86.046947,207.473627 Z M96.6021471,207.069023 C96.7844366,208.099171 95.7267341,209.156872 94.215428,209.438785 C92.7295577,209.710099 91.3539086,209.074206 91.1652603,208.052538 C90.9808515,206.996955 92.0576306,205.939253 93.5413813,205.66582 C95.054807,205.402984 96.4092596,206.021919 96.6021471,207.069023 Z"
|
||||
fill="#161614"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
26
webapp/public/assets/auth_providers/google.svg
Normal file
26
webapp/public/assets/auth_providers/google.svg
Normal file
@ -0,0 +1,26 @@
|
||||
<svg
|
||||
viewBox="0 0 256 262"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M255.878,133.451 C255.878,122.717 255.007,114.884 253.122,106.761 L130.55,106.761 L130.55,155.209 L202.497,155.209 C201.047,167.249 193.214,185.381 175.807,197.565 L175.563,199.187 L214.318,229.21 L217.003,229.478 C241.662,206.704 255.878,173.196 255.878,133.451"
|
||||
fill="#4285F4"
|
||||
></path>
|
||||
<path
|
||||
d="M130.55,261.1 C165.798,261.1 195.389,249.495 217.003,229.478 L175.807,197.565 C164.783,205.253 149.987,210.62 130.55,210.62 C96.027,210.62 66.726,187.847 56.281,156.37 L54.75,156.5 L14.452,187.687 L13.925,189.152 C35.393,231.798 79.49,261.1 130.55,261.1"
|
||||
fill="#34A853"
|
||||
></path>
|
||||
<path
|
||||
d="M56.281,156.37 C53.525,148.247 51.93,139.543 51.93,130.55 C51.93,121.556 53.525,112.853 56.136,104.73 L56.063,103 L15.26,71.312 L13.925,71.947 C5.077,89.644 0,109.517 0,130.55 C0,151.583 5.077,171.455 13.925,189.152 L56.281,156.37"
|
||||
fill="#FBBC05"
|
||||
></path>
|
||||
<path
|
||||
d="M130.55,50.479 C155.064,50.479 171.6,61.068 181.029,69.917 L217.873,33.943 C195.245,12.91 165.798,0 130.55,0 C79.49,0 35.393,29.301 13.925,71.947 L56.136,104.73 C66.726,73.253 96.027,50.479 130.55,50.479"
|
||||
fill="#EB4335"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
22
webapp/public/assets/auth_providers/slack.svg
Normal file
22
webapp/public/assets/auth_providers/slack.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<svg
|
||||
viewBox="0 0 256 250"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<path
|
||||
d="M53.841 161.32c0 14.832-11.987 26.82-26.819 26.82-14.832 0-26.819-11.988-26.819-26.82 0-14.831 11.987-26.818 26.82-26.818H53.84v26.819zm13.41 0c0-14.831 11.987-26.818 26.819-26.818 14.832 0 26.819 11.987 26.819 26.819v67.047c0 14.832-11.987 26.82-26.82 26.82-14.83 0-26.818-11.988-26.818-26.82v-67.047z"
|
||||
fill="#E01E5A"
|
||||
/>
|
||||
<path
|
||||
d="M94.07 53.638c-14.832 0-26.82-11.987-26.82-26.819C67.25 11.987 79.239 0 94.07 0s26.819 11.987 26.819 26.819v26.82h-26.82zm0 13.613c14.832 0 26.819 11.987 26.819 26.819 0 14.832-11.987 26.819-26.82 26.819H26.82C11.987 120.889 0 108.902 0 94.069c0-14.83 11.987-26.818 26.819-26.818h67.25z"
|
||||
fill="#36C5F0"
|
||||
/>
|
||||
<path
|
||||
d="M201.55 94.07c0-14.832 11.987-26.82 26.818-26.82 14.832 0 26.82 11.988 26.82 26.82s-11.988 26.819-26.82 26.819H201.55v-26.82zm-13.41 0c0 14.832-11.988 26.819-26.82 26.819-14.831 0-26.818-11.987-26.818-26.82V26.82C134.502 11.987 146.489 0 161.32 0c14.831 0 26.819 11.987 26.819 26.819v67.25z"
|
||||
fill="#2EB67D"
|
||||
/>
|
||||
<path
|
||||
d="M161.32 201.55c14.832 0 26.82 11.987 26.82 26.818 0 14.832-11.988 26.82-26.82 26.82-14.831 0-26.818-11.988-26.818-26.82V201.55h26.819zm0-13.41c-14.831 0-26.818-11.988-26.818-26.82 0-14.831 11.987-26.818 26.819-26.818h67.25c14.832 0 26.82 11.987 26.82 26.819 0 14.831-11.988 26.819-26.82 26.819h-67.25z"
|
||||
fill="#ECB22E"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.5 KiB |
Loading…
Reference in New Issue
Block a user