-
Notifications
You must be signed in to change notification settings - Fork 56
Implement /auth with IVLE token #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
89cccb5
4a9d700
fac9126
2a2d8ba
28fe00d
62c5817
4190763
3e8a27d
7a7e055
b9a7a86
7a6a046
263296b
a9925f8
68c56c8
edad9d3
0eef9d0
20c465e
5267c28
dab8eb5
753d82a
66808a2
0b0e77e
cbe625e
b21dbdb
9ee39a1
1d823db
797b7b1
73de001
634ccd7
f11f519
6298799
3508e83
2c2cede
f1e09aa
2a5329a
eaa77fc
0a9c589
59a315f
ae97a8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,21 +4,21 @@ defmodule Cadet.Accounts do | |
| """ | ||
| use Cadet, :context | ||
|
|
||
| alias Comeonin.Pbkdf2 | ||
|
|
||
| alias Cadet.Accounts.Authorization | ||
| alias Cadet.Accounts.IVLE | ||
| alias Cadet.Accounts.User | ||
| alias Cadet.Accounts.Query | ||
| alias Cadet.Accounts.Authorization | ||
| alias Cadet.Accounts.Form.Registration | ||
|
|
||
| @doc """ | ||
| Register new User entity using E-mail and Password | ||
| authentication. | ||
| Register new User entity using Cadet.Accounts.Form.Registration | ||
|
|
||
| Returns {:ok, user} on success, otherwise {:error, changeset} | ||
| """ | ||
| def register(attrs = %{}, role) do | ||
| changeset = Registration.changeset(%Registration{}, attrs) | ||
|
|
||
| if changeset.valid?() do | ||
| if changeset.valid? do | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @indocomsoft The new style guide in #50 says that zero-arity calls should end with parenthesis, but I think predicate functions should be excluded, because their trailing
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, in the first place, |
||
| registration = apply_changes(changeset) | ||
|
|
||
| Repo.transaction(fn -> | ||
|
|
@@ -28,9 +28,8 @@ defmodule Cadet.Accounts do | |
| {:ok, _} = | ||
| create_authorization( | ||
| %{ | ||
| provider: :email, | ||
| uid: registration.email, | ||
| token: Pbkdf2.hashpwsalt(registration.password) | ||
| provider: :nusnet_id, | ||
| uid: registration.nusnet_id | ||
| }, | ||
| user | ||
| ) | ||
|
|
@@ -69,77 +68,59 @@ defmodule Cadet.Accounts do | |
| end | ||
|
|
||
| @doc """ | ||
| Associate an e-mail address with an existing `%User{}` | ||
| The user will be able to authenticate using the e-mail | ||
| Associate NUSTNET_ID with an existing `%User{}` | ||
| """ | ||
| def add_email(user = %User{}, email) do | ||
| token = get_token(:email, email) || get_random_token() | ||
|
|
||
| def add_nusnet_id(user = %User{}, nusnet_id) do | ||
| changeset = | ||
| %Authorization{} | ||
| |> Authorization.changeset(%{ | ||
| provider: :email, | ||
| uid: email, | ||
| token: token | ||
| provider: :nusnet_id, | ||
| uid: nusnet_id | ||
| }) | ||
| |> put_assoc(:user, user) | ||
|
|
||
| Repo.insert(changeset) | ||
| end | ||
|
|
||
| @doc """ | ||
| Associate a password to an existing `%User{}` | ||
| The user will be able to authenticate using any of the e-mail | ||
| and the password. | ||
| Associate a NUSNET_ID to an existing `%User{}` | ||
| """ | ||
| def set_password(user = %User{}, password) do | ||
| token = Pbkdf2.hashpwsalt(password) | ||
|
|
||
| def set_nusnet_id(user = %User{}, nusnet_id) do | ||
| Repo.transaction(fn -> | ||
| authorizations = Repo.all(Query.user_emails(user.id)) | ||
| authorizations = Repo.all(Query.user_nusnet_ids(user.id)) | ||
|
|
||
| for email <- authorizations do | ||
| |> change(%{token: token}) | ||
| for authorization <- authorizations do | ||
| authorization | ||
| |> change(%{nusnet_id: nusnet_id}) | ||
| |> Repo.update!() | ||
| end | ||
| end) | ||
| end | ||
|
|
||
| @doc """ | ||
| Sign in using given e-mail and password combination | ||
| Sign in using given NUSNET_ID | ||
| """ | ||
| def sign_in(email, password) do | ||
| auth = Repo.one(Query.email(email)) | ||
|
|
||
| cond do | ||
| auth == nil -> | ||
| {:error, :not_found} | ||
|
|
||
| not Pbkdf2.checkpw(password, auth.token) -> | ||
| {:error, :invalid_password} | ||
| def sign_in(nusnet_id, token) do | ||
| auth = Repo.one(Query.nusnet_id(nusnet_id)) | ||
|
|
||
| true -> | ||
| auth = Repo.preload(auth, :user) | ||
| {:ok, auth.user} | ||
| end | ||
| end | ||
|
|
||
| defp get_token(provider, uid) do | ||
| auth = Repo.get_by(Authorization, provider: provider, uid: uid) | ||
|
|
||
| if auth == nil do | ||
| nil | ||
| if auth do | ||
| auth = Repo.preload(auth, :user) | ||
| {:ok, auth.user} | ||
| else | ||
| auth.token | ||
| # user is not registered in our database | ||
| with {:ok, name} <- IVLE.fetch_name(token), | ||
| {:ok, _} <- register(%{name: name, nusnet_id: nusnet_id}, :student) do | ||
| sign_in(nusnet_id, token) | ||
| else | ||
| {:error, :bad_request} -> | ||
| # IVLE.fetch_name/1 responds with :bad_request if token is invalid | ||
| {:error, :bad_request} | ||
|
|
||
| {:error, _} -> | ||
| # IVLE.fetch_name/1 responds with :internal_server_error if API key is invalid | ||
| # register/2 returns {:error, changeset} if changeset is invalid | ||
| {:error, :internal_server_error} | ||
| end | ||
| end | ||
| end | ||
|
|
||
| defp get_random_token() do | ||
| length = 64 | ||
|
|
||
| :crypto.strong_rand_bytes(length) | ||
| |> Base.url_encode64() | ||
| |> binary_part(0, length) | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,33 +1,24 @@ | ||
| defmodule Cadet.Accounts.Form.Registration do | ||
| @moduledoc """ | ||
| The Accounts.Form entity represents an entry from an accounts form. | ||
| A registration form contains the same information as the User and Authorization | ||
| entity, including first name, last name, e-mail, password and password | ||
| confirmation. | ||
| The Accounts.Form entity represents an entry from a /auth call, where the | ||
| IVLE authentication token corresponds to a user who has not been registered | ||
| in our database. | ||
| """ | ||
|
|
||
| use Ecto.Schema | ||
|
|
||
| import Ecto.Changeset | ||
|
|
||
| embedded_schema do | ||
| field(:first_name, :string) | ||
| field(:last_name, :string) | ||
| field(:email, :string) | ||
| field(:password, :string) | ||
| field(:password_confirmation, :string) | ||
| field(:name, :string) | ||
| field(:nusnet_id, :string) | ||
| end | ||
|
|
||
| @required_fields ~w(first_name email password password_confirmation)a | ||
| @optional_fields ~w(last_name)a | ||
|
|
||
| @email_format ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/ | ||
| @required_fields ~w(name nusnet_id)a | ||
|
|
||
| def changeset(registration, params \\ %{}) do | ||
| registration | ||
| |> cast(params, @required_fields ++ @optional_fields) | ||
| |> cast(params, @required_fields) | ||
| |> validate_required(@required_fields) | ||
| |> validate_format(:email, @email_format) | ||
| |> validate_length(:password, min: 8) | ||
| |> validate_confirmation(:password) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| defmodule Cadet.Accounts.IVLE do | ||
| @moduledoc """ | ||
| Helper functions to IVLE calls. All helper functions are prefixed with fetch | ||
| to differentiate them from database helpers, or other 'getters'. | ||
| This module relies on the environment variable `IVLE_KEY` being set. | ||
| `IVLE_KEY` should contain the IVLE Lapi key. Obtain the key from | ||
| [this link](http://ivle.nus.edu.sg/LAPI/default.aspx). | ||
| """ | ||
|
|
||
| @api_url "https://ivle.nus.edu.sg/api/Lapi.svc" | ||
| @api_key Dotenv.load().values["IVLE_KEY"] | ||
|
|
||
| @doc """ | ||
| Get the NUSNET ID of the user corresponding to this token. | ||
| returns... | ||
| - {:ok, nusnet_id} - valid token, nusnet_id is a string | ||
| - {:error, :bad_request} - invalid token | ||
| - {:error, :internal_server_error} - the ivle_key is invalid | ||
| ## Parameters | ||
| - token: String, the IVLE authentication token | ||
| ## Examples | ||
| iex> Cadet.Accounts.IVLE.fetch_nusnet_id("T0K3N...") | ||
| {:ok, "e012345"} | ||
| """ | ||
| def fetch_nusnet_id(token), do: api_fetch("UserID_Get", token) | ||
|
|
||
| @doc """ | ||
| Get the full name of the user corresponding to this token. | ||
| returns... | ||
| - {:ok, username} - valid token, username is a string | ||
| - {:error, :bad_request} - invalid token | ||
| - {:error, :internal_server_error} - the ivle_key is invalid | ||
| ## Parameters | ||
| - token: String, the IVLE authentication token | ||
| ## Examples | ||
| iex> Cadet.Accounts.IVLE.fetch_name("T0K3N...") | ||
| {:ok, "LEE NING YUAN"} | ||
| """ | ||
| def fetch_name(token), do: api_fetch("UserName_Get", token) | ||
|
|
||
| defp api_fetch(path, token) do | ||
| case HTTPoison.get(api_url(path, token)) do | ||
| {:ok, %{body: body, status_code: 200}} when body != ~s("") -> | ||
| {:ok, Poison.decode!(body)} | ||
|
|
||
| {:ok, %{status_code: 500}} -> | ||
| # IVLE responds with 500 if APIKey is invalid | ||
| {:error, :internal_server_error} | ||
|
|
||
| {:ok, %{body: ~s(""), status_code: 200}} -> | ||
| # IVLE responsed 200 with body == ~s("") if token is invalid | ||
| {:error, :bad_request} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you should handle the case
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha, done with 0a9c589. |
||
| end | ||
| end | ||
|
|
||
| defp api_url(path, token) do | ||
| # construct a valid URL with the module attributes, and given params | ||
| "#{@api_url}/#{path}?APIKey=#{@api_key}&Token=#{token}" | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import EctoEnum | ||
|
|
||
| defenum(Cadet.Accounts.Provider, :provider, [ | ||
| # Email provides e-mail and password based authorization | ||
| # An IVLE authentication token provides a NUSNET ID to track user identity | ||
| :nusnet_id | ||
| ]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to rename this to
.env.exampleand add instruction in README to rename this to.envand modify the content accordingly.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with 8d017f9, and README updated accordingly.