diff --git a/js/src/components/OAuth/AuthorizeApplication.vue b/js/src/components/OAuth/AuthorizeApplication.vue new file mode 100644 index 00000000..913ef044 --- /dev/null +++ b/js/src/components/OAuth/AuthorizeApplication.vue @@ -0,0 +1,91 @@ + + + diff --git a/js/src/graphql/application.ts b/js/src/graphql/application.ts index 11e9849b..086a8579 100644 --- a/js/src/graphql/application.ts +++ b/js/src/graphql/application.ts @@ -3,6 +3,7 @@ import gql from "graphql-tag"; export const AUTH_APPLICATION = gql` query AuthApplication($clientId: String!) { authApplication(clientId: $clientId) { + id clientId name website @@ -13,7 +14,7 @@ export const AUTH_APPLICATION = gql` export const AUTORIZE_APPLICATION = gql` mutation AuthorizeApplication( $applicationClientId: String! - $redirectURI: String! + $redirectURI: String $state: String $scope: String ) { @@ -53,3 +54,18 @@ export const REVOKED_AUTHORIZED_APPLICATION = gql` } } `; + +export const DEVICE_ACTIVATION = gql` + mutation DeviceActivation($userCode: String!) { + deviceActivation(userCode: $userCode) { + id + application { + id + clientId + name + website + } + scope + } + } +`; diff --git a/js/src/router/user.ts b/js/src/router/user.ts index 612c4cce..cd80c297 100644 --- a/js/src/router/user.ts +++ b/js/src/router/user.ts @@ -14,6 +14,7 @@ export enum UserRouteName { VALIDATE = "Validate", LOGIN = "Login", OAUTH_AUTORIZE = "OAUTH_AUTORIZE", + OAUTH_LOGIN_DEVICE = "OAUTH_LOGIN_DEVICE", } export const userRoutes: RouteRecordRaw[] = [ @@ -120,4 +121,16 @@ export const userRoutes: RouteRecordRaw[] = [ }, }, }, + { + path: "/login/device", + name: UserRouteName.OAUTH_LOGIN_DEVICE, + component: (): Promise => + import("@/views/OAuth/DeviceActivationView.vue"), + meta: { + requiredAuth: true, + announcer: { + message: (): string => t("Device activation") as string, + }, + }, + }, ]; diff --git a/js/src/views/OAuth/AuthorizeView.vue b/js/src/views/OAuth/AuthorizeView.vue index 618ce49e..184c5fdc 100644 --- a/js/src/views/OAuth/AuthorizeView.vue +++ b/js/src/views/OAuth/AuthorizeView.vue @@ -26,39 +26,14 @@ -
-

- {{ t("Autorize this application to access your account?") }} -

- -
- -

- {{ - t( - "This application will be able to access all of your informations and post content on your behalf. Make sure you only approve applications you trust." - ) - }} -

-
- -
-
-

{{ authApplication?.name }}

-

{{ authApplication?.website }}

-
-
- {{ t("Authorize") }} - {{ - t("Decline") - }} -
-
-
+ :auth-application="authApplication" + :redirectURI="redirectURI" + :state="state" + :scope="scope" + />
+
+
+

+ {{ t("Device activation") }} +

+

+ {{ t("Enter the code displayed on your device") }} +

+ +
+
+ - + +
+
+ +
+ +
+

{{ error }}

+
+
+ +
+ {{ t("Continue") }} +
+
+ +
+ + + diff --git a/js/src/views/Settings/AppsView.vue b/js/src/views/Settings/AppsView.vue index 35ef4ccf..21bf2c3d 100644 --- a/js/src/views/Settings/AppsView.vue +++ b/js/src/views/Settings/AppsView.vue @@ -76,11 +76,12 @@ import { } from "@/graphql/application"; import { useMutation, useQuery } from "@vue/apollo-composable"; import { useHead } from "@vueuse/head"; -import { computed } from "vue"; +import { computed, inject } from "vue"; import { useI18n } from "vue-i18n"; import RouteName from "../../router/name"; import { IUser } from "@/types/current-user.model"; import { formatDateString } from "@/filters/datetime"; +import { Notifier } from "@/plugins/notifier"; const { t } = useI18n({ useScope: "global" }); @@ -132,6 +133,14 @@ const { mutate: revoke, onDone: onRevokedApplication } = useMutation< }, }); +const notifier = inject("notifier"); + +onRevokedApplication(() => { + notifier?.success( + t("Application was revoked") + ); +}) + useHead({ title: computed(() => t("Apps")), }); diff --git a/lib/graphql/resolvers/application.ex b/lib/graphql/resolvers/application.ex index a546148d..0b17a580 100644 --- a/lib/graphql/resolvers/application.ex +++ b/lib/graphql/resolvers/application.ex @@ -4,7 +4,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Application do """ alias Mobilizon.Applications, as: ApplicationManager - alias Mobilizon.Applications.{Application, ApplicationToken} + alias Mobilizon.Applications.{Application, ApplicationDeviceActivation, ApplicationToken} alias Mobilizon.Service.Auth.Applications alias Mobilizon.Users.User import Mobilizon.Web.Gettext, only: [dgettext: 2] @@ -89,4 +89,33 @@ defmodule Mobilizon.GraphQL.Resolvers.Application do def revoke_application_token(_parent, _args, _resolution) do {:error, :unauthenticated} end + + def activate_device(_parent, %{user_code: user_code}, %{ + context: %{current_user: %User{} = user} + }) do + with {:ok, %ApplicationDeviceActivation{} = app_device_activation} <- + Applications.activate_device(user_code, user) do + {:ok, app_device_activation |> Map.from_struct() |> Map.take([:application, :id, :scope])} + end + end + + @spec authorize_device_application(any(), map(), Absinthe.Resolution.t()) :: + {:ok, map()} | {:error, String.t()} + def authorize_device_application( + _parent, + %{client_id: client_id, user_code: user_code}, + %{context: %{current_user: %User{id: user_id}}} + ) do + case Applications.autorize_device_application(client_id, user_code, user_id) do + {:ok, %Application{} = app} -> + {:ok, app} + + {:error, :application_not_found} -> + {:error, + dgettext( + "errors", + "No application with this client_id was found" + )} + end + end end diff --git a/lib/graphql/schema/auth_application.ex b/lib/graphql/schema/auth_application.ex index 5cdda92a..37fba37a 100644 --- a/lib/graphql/schema/auth_application.ex +++ b/lib/graphql/schema/auth_application.ex @@ -7,6 +7,7 @@ defmodule Mobilizon.GraphQL.Schema.AuthApplicationType do @desc "An application" object :auth_application do + field(:id, :id) field(:name, :string) field(:client_id, :string) field(:scopes, :string) @@ -27,6 +28,12 @@ defmodule Mobilizon.GraphQL.Schema.AuthApplicationType do field(:state, :string) end + object :application_device_activation do + field(:id, :id) + field(:application, :auth_application) + field(:scope, :string) + end + object :auth_application_queries do @desc "Get an application" field :auth_application, :auth_application do @@ -53,9 +60,27 @@ defmodule Mobilizon.GraphQL.Schema.AuthApplicationType do resolve(&Application.authorize/3) end + @desc "Revoke an authorized application" field :revoke_application_token, :deleted_object do arg(:app_token_id, non_null(:string), description: "The application token's ID") resolve(&Application.revoke_application_token/3) end + + @desc "Activate an user device" + field :device_activation, :application_device_activation do + arg(:user_code, non_null(:string), + description: "The code provided by the application entered by the user" + ) + + resolve(&Application.activate_device/3) + end + + @desc "Activate an user device" + field :authorize_device_application, :auth_application do + arg(:client_id, non_null(:string), description: "The application's client_id") + arg(:scope, :string, description: "The scope for the authorization") + + resolve(&Application.authorize_device_application/3) + end end end diff --git a/lib/mobilizon/applications.ex b/lib/mobilizon/applications.ex index b0df2539..60425097 100644 --- a/lib/mobilizon/applications.ex +++ b/lib/mobilizon/applications.ex @@ -4,10 +4,24 @@ defmodule Mobilizon.Applications do """ import Ecto.Query, warn: false + import EctoEnum alias Ecto.Multi alias Mobilizon.Applications.Application alias Mobilizon.Storage.Repo + defenum(ApplicationDeviceActivationStatus, [ + "success", + "pending", + "incorrect_device_code", + "access_denied" + ]) + + defenum(ApplicationTokenStatus, [ + "success", + "pending", + "access_denied" + ]) + @doc """ Returns the list of applications. @@ -255,4 +269,129 @@ defmodule Mobilizon.Applications do def change_application_token(%ApplicationToken{} = application_token, attrs \\ %{}) do ApplicationToken.changeset(application_token, attrs) end + + alias Mobilizon.Applications.ApplicationDeviceActivation + + @doc """ + Returns the list of application_device_activation. + + ## Examples + + iex> list_application_device_activation() + [%ApplicationDeviceActivation{}, ...] + + """ + def list_application_device_activation do + Repo.all(ApplicationDeviceActivation) + end + + @doc """ + Gets a single application_device_activation. + + Raises `Ecto.NoResultsError` if the Application device activation does not exist. + + ## Examples + + iex> get_application_device_activation!(123) + %ApplicationDeviceActivation{} + + iex> get_application_device_activation!(456) + ** (Ecto.NoResultsError) + + """ + def get_application_device_activation!(id), do: Repo.get!(ApplicationDeviceActivation, id) + + def get_application_device_activation(id), do: Repo.get(ApplicationDeviceActivation, id) + + def get_application_device_activation_by_user_code(user_code), + do: Repo.get_by(ApplicationDeviceActivation, user_code: user_code) + + def get_application_device_activation(client_id, device_code) do + ApplicationDeviceActivation + |> join(:left, [ada], a in assoc(ada, :application)) + |> where([_, a], a.client_id == ^client_id) + |> where([ada], ada.device_code == ^device_code) + |> select([ada], ada) + |> Repo.one() + end + + @doc """ + Creates a application_device_activation. + + ## Examples + + iex> create_application_device_activation(%{field: value}) + {:ok, %ApplicationDeviceActivation{}} + + iex> create_application_device_activation(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_application_device_activation(attrs \\ %{}) do + %ApplicationDeviceActivation{} + |> ApplicationDeviceActivation.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a application_device_activation. + + ## Examples + + iex> update_application_device_activation(application_device_activation, %{field: new_value}) + {:ok, %ApplicationDeviceActivation{}} + + iex> update_application_device_activation(application_device_activation, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_application_device_activation( + %ApplicationDeviceActivation{} = application_device_activation, + attrs + ) do + application_device_activation + |> ApplicationDeviceActivation.changeset(attrs) + |> Repo.update() + |> case do + {:ok, application_device_activation} -> + {:ok, Repo.preload(application_device_activation, :application)} + + error -> + error + end + end + + @doc """ + Deletes a application_device_activation. + + ## Examples + + iex> delete_application_device_activation(application_device_activation) + {:ok, %ApplicationDeviceActivation{}} + + iex> delete_application_device_activation(application_device_activation) + {:error, %Ecto.Changeset{}} + + """ + def delete_application_device_activation( + %ApplicationDeviceActivation{} = application_device_activation + ) do + Repo.delete(application_device_activation) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking application_device_activation changes. + + ## Examples + + iex> change_application_device_activation(application_device_activation) + %Ecto.Changeset{data: %ApplicationDeviceActivation{}} + + """ + def change_application_device_activation( + %ApplicationDeviceActivation{} = application_device_activation, + attrs \\ %{} + ) do + ApplicationDeviceActivation.changeset(application_device_activation, attrs) + end end diff --git a/lib/mobilizon/applications/application_device_activation.ex b/lib/mobilizon/applications/application_device_activation.ex new file mode 100644 index 00000000..27f0de0a --- /dev/null +++ b/lib/mobilizon/applications/application_device_activation.ex @@ -0,0 +1,32 @@ +defmodule Mobilizon.Applications.ApplicationDeviceActivation do + @moduledoc """ + Module representing a application device activation + """ + use Ecto.Schema + import Ecto.Changeset + alias Mobilizon.Applications.{Application, ApplicationDeviceActivationStatus} + alias Mobilizon.Users.User + + schema "application_device_activation" do + field(:user_code, :string) + field(:device_code, :string) + field(:scope, :string) + field(:expires_in, :integer) + field(:status, ApplicationDeviceActivationStatus, default: :pending) + belongs_to(:user, User) + belongs_to(:application, Application) + + timestamps() + end + + @required_attrs [:user_code, :device_code, :expires_in, :application_id] + @optional_attrs [:status, :user_id] + @attrs @required_attrs ++ @optional_attrs + + @doc false + def changeset(application_device_activation, attrs) do + application_device_activation + |> cast(attrs, @attrs) + |> validate_required(@required_attrs) + end +end diff --git a/lib/mobilizon/applications/application_token.ex b/lib/mobilizon/applications/application_token.ex index 90d94b42..bcf3575c 100644 --- a/lib/mobilizon/applications/application_token.ex +++ b/lib/mobilizon/applications/application_token.ex @@ -4,16 +4,20 @@ defmodule Mobilizon.Applications.ApplicationToken do """ use Ecto.Schema import Ecto.Changeset + alias Mobilizon.Applications.{Application, ApplicationTokenStatus} + alias Mobilizon.Users.User schema "application_tokens" do - belongs_to(:user, Mobilizon.Users.User) - belongs_to(:application, Mobilizon.Applications.Application) + belongs_to(:user, User) + belongs_to(:application, Application) field(:authorization_code, :string) + field(:status, ApplicationTokenStatus) + field(:scope, :string) timestamps() end - @required_attrs [:user_id, :application_id] + @required_attrs [:user_id, :application_id, :scope] @optional_attrs [:authorization_code] @attrs @required_attrs ++ @optional_attrs diff --git a/lib/service/auth/applications.ex b/lib/service/auth/applications.ex index 252f5c25..c77538f1 100644 --- a/lib/service/auth/applications.ex +++ b/lib/service/auth/applications.ex @@ -3,8 +3,10 @@ defmodule Mobilizon.Service.Auth.Applications do Module to handle applications management """ alias Mobilizon.Applications - alias Mobilizon.Applications.{Application, ApplicationToken} + alias Mobilizon.Applications.{Application, ApplicationDeviceActivation, ApplicationToken} alias Mobilizon.Service.Auth.Authenticator + alias Mobilizon.Users.User + alias Mobilizon.Web.Router.Helpers, as: Routes @app_access_tokens_ttl {8, :hour} @app_refresh_tokens_ttl {26, :week} @@ -58,6 +60,15 @@ defmodule Mobilizon.Service.Auth.Applications do end end + def autorize_device_application(client_id, user_code) do + case Applications.get_application_device_activation(client_id, user_code) do + %ApplicationDeviceActivation{status: :confirmed} = app_device_activation -> + Applications.update_application_device_activation(app_device_activation, %{ + status: :success + }) + end + end + @spec generate_access_token(String.t(), String.t(), String.t(), String.t()) :: {:ok, access_token_details()} | {:error, @@ -118,6 +129,126 @@ defmodule Mobilizon.Service.Auth.Applications do end end + def generate_access_token(client_id, device_code) do + case Applications.get_application_device_activation(client_id, device_code) do + %ApplicationDeviceActivation{status: :success, scope: scope, user_id: user_id} = + app_device_activation -> + if device_activation_expired?(app_device_activation) do + {:error, :expired} + else + %Application{id: app_id} = Applications.get_application_by_client_id(client_id) + + {:ok, %ApplicationToken{} = app_token} = + Applications.create_application_token(%{ + user_id: user_id, + application_id: app_id, + authorization_code: nil, + scope: scope + }) + + {:ok, access_token} = + Authenticator.generate_access_token(app_token, @app_access_tokens_ttl) + + {:ok, refresh_token} = + Authenticator.generate_refresh_token(app_token, @app_refresh_tokens_ttl) + + {:ok, + %{ + access_token: access_token, + expires_in: ttl_to_seconds(@app_access_tokens_ttl), + refresh_token: refresh_token, + refresh_token_expires_in: ttl_to_seconds(@app_refresh_tokens_ttl), + scope: scope, + token_type: "bearer" + }} + end + + %ApplicationDeviceActivation{status: :incorrect_device_code} -> + {:error, :incorrect_device_code} + + %ApplicationDeviceActivation{status: :access_denied} -> + {:error, :access_denied} + + err -> + require Logger + Logger.error(inspect(err)) + {:error, :incorrect_device_code} + end + end + + @chars "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |> String.split("", trim: true) + + defp string_of_length(length) do + 1..length + |> Enum.reduce([], fn _i, acc -> + [Enum.random(@chars) | acc] + end) + |> Enum.join("") + end + + @expires_in 900 + @interval 5 + + @spec register_device_code(String.t(), String.t() | nil) :: + {:ok, ApplicationDeviceActivation.t()} + | {:error, Ecto.Changeset.t()} + def register_device_code(client_id, scope) do + %Application{} = application = Applications.get_application_by_client_id(client_id) + device_code = string_of_length(40) + user_code = string_of_length(8) + verification_uri = Routes.page_url(Mobilizon.Web.Endpoint, :auth_device) + expires_in = @expires_in + interval = @interval + + case Applications.create_application_device_activation(%{ + device_code: device_code, + user_code: user_code, + expires_in: expires_in, + application_id: application.id, + scope: scope + }) do + {:ok, %ApplicationDeviceActivation{} = application_device_activation} -> + {:ok, + application_device_activation + |> Map.from_struct() + |> Map.take([:device_code, :user_code, :expires_in]) + |> Map.update!(:user_code, &user_code_displayed/1) + |> Map.merge(%{ + interval: interval, + verification_uri: verification_uri + })} + + {:error, %Ecto.Changeset{} = err} -> + {:error, err} + end + end + + @spec activate_device(String.t(), User.t()) :: + {:ok, ApplicationDeviceActivation.t()} + | {:error, Ecto.Changeset.t()} + | {:error, :not_found} + | {:error, :expired} + def activate_device(user_code, user) do + case Applications.get_application_device_activation_by_user_code(user_code) do + %ApplicationDeviceActivation{} = app_device_activation -> + if device_activation_expired?(app_device_activation) do + {:error, :expired} + else + Applications.update_application_device_activation(app_device_activation, %{ + status: :confirmed, + user_id: user.id + }) + end + + _ -> + {:error, :not_found} + end + end + + defp user_code_displayed(user_code) do + String.slice(user_code, 0..3) <> "-" <> String.slice(user_code, 4..7) + end + def revoke_application_token(%ApplicationToken{} = app_token) do Applications.revoke_application_token(app_token) end @@ -127,4 +258,13 @@ defmodule Mobilizon.Service.Auth.Applications do defp ttl_to_seconds({value, :minute}), do: value * 60 defp ttl_to_seconds({value, :hour}), do: value * 3600 defp ttl_to_seconds({value, :week}), do: value * 604_800 + + @spec device_activation_expired?(ApplicationDeviceActivation.t()) :: boolean() + defp device_activation_expired?(%ApplicationDeviceActivation{ + inserted_at: inserted_at, + expires_in: expires_in + }) do + NaiveDateTime.compare(NaiveDateTime.add(inserted_at, expires_in), NaiveDateTime.utc_now()) == + :gt + end end diff --git a/lib/web/controllers/application_controller.ex b/lib/web/controllers/application_controller.ex index 63e0bfb2..df26c99b 100644 --- a/lib/web/controllers/application_controller.ex +++ b/lib/web/controllers/application_controller.ex @@ -1,12 +1,11 @@ defmodule Mobilizon.Web.ApplicationController do use Mobilizon.Web, :controller - alias Mobilizon.Applications.Application + alias Mobilizon.Applications.{Application, ApplicationDeviceActivation} alias Mobilizon.Service.Auth.Applications plug(:put_layout, false) import Mobilizon.Web.Gettext, only: [dgettext: 2] - - @out_of_band_redirect_uri "urn:ietf:wg:oauth:2.0:oob" + require Logger @doc """ Create an application @@ -84,6 +83,27 @@ defmodule Mobilizon.Web.ApplicationController do end end + def device_code(conn, %{"client_id" => client_id} = args) do + case Applications.register_device_code(client_id, Map.get(args, "scope")) do + {:ok, res} when is_map(res) -> + case get_format(conn) do + "json" -> + json(conn, res) + + _ -> + send_resp(conn, 200, URI.encode_query(res)) + end + + {:error, %Ecto.Changeset{} = err} -> + Logger.error(inspect(err)) + send_resp(conn, 500, "Unable to produce device code") + end + end + + def device_code(conn, _args) do + send_resp(conn, 400, "You need to send to send at least client_id to obtain a device code") + end + @spec generate_access_token(Plug.Conn.t(), map()) :: Plug.Conn.t() def generate_access_token(conn, %{ "client_id" => client_id, @@ -93,11 +113,7 @@ defmodule Mobilizon.Web.ApplicationController do }) do case Applications.generate_access_token(client_id, client_secret, code, redirect_uri) do {:ok, token} -> - if redirect_uri != @out_of_band_redirect_uri do - redirect(conn, external: generate_redirect_with_query_params(redirect_uri, token)) - else - json(conn, token) - end + redirect(conn, external: generate_redirect_with_query_params(redirect_uri, token)) {:error, :application_not_found} -> send_resp(conn, 400, dgettext("errors", "No application was found with this client_id")) @@ -123,6 +139,27 @@ defmodule Mobilizon.Web.ApplicationController do end end + def generate_access_token(conn, %{ + "client_id" => client_id, + "device_code" => device_code, + "grant_type" => "urn:ietf:params:oauth:grant-type:device_code", + "_format" => "json" + }) do + json(conn, Applications.generate_access_token(client_id, device_code)) + end + + def generate_access_token(conn, %{ + "client_id" => client_id, + "device_code" => device_code, + "grant_type" => "urn:ietf:params:oauth:grant-type:device_code" + }) do + send_resp( + conn, + 200, + URI.encode_query(Applications.generate_access_token(client_id, device_code)) + ) + end + @spec generate_redirect_with_query_params(String.t(), map()) :: String.t() defp generate_redirect_with_query_params(redirect_uri, query_params) do redirect_uri |> URI.parse() |> URI.merge("?" <> URI.encode_query(query_params)) |> to_string() diff --git a/lib/web/controllers/page_controller.ex b/lib/web/controllers/page_controller.ex index 5243eec6..798d91aa 100644 --- a/lib/web/controllers/page_controller.ex +++ b/lib/web/controllers/page_controller.ex @@ -124,6 +124,9 @@ defmodule Mobilizon.Web.PageController do @spec authorize(Plug.Conn.t(), any) :: Plug.Conn.t() def authorize(conn, _params), do: render(conn, :index) + @spec auth_device(Plug.Conn.t(), any) :: Plug.Conn.t() + def auth_device(conn, _params), do: render(conn, :index) + @spec handle_collection_route(Plug.Conn.t(), collections()) :: Plug.Conn.t() defp handle_collection_route(conn, collection) do case get_format(conn) do diff --git a/lib/web/router.ex b/lib/web/router.ex index c8531447..97907aed 100644 --- a/lib/web/router.ex +++ b/lib/web/router.ex @@ -210,6 +210,17 @@ defmodule Mobilizon.Web.Router do get("/oauth/authorize", ApplicationController, :authorize) post("/oauth/token", ApplicationController, :generate_access_token) get("/oauth/autorize_approve", PageController, :authorize) + get("/login/device", PageController, :auth_device) + end + + pipeline :login do + plug(:accepts, ["html", "json"]) + end + + scope "/login", Mobilizon.Web do + pipe_through(:login) + + post("/device/code", ApplicationController, :device_code) end scope "/proxy/", Mobilizon.Web do diff --git a/priv/repo/migrations/20230216151638_add_device_flow_support.exs b/priv/repo/migrations/20230216151638_add_device_flow_support.exs new file mode 100644 index 00000000..2e83dc59 --- /dev/null +++ b/priv/repo/migrations/20230216151638_add_device_flow_support.exs @@ -0,0 +1,10 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AddDeviceFlowSupport do + use Ecto.Migration + + def change do + alter table(:application_tokens) do + add(:status, :string, default: :pending, null: false) + add(:scope, :string) + end + end +end diff --git a/priv/repo/migrations/20230217084253_create_application_device_activation.exs b/priv/repo/migrations/20230217084253_create_application_device_activation.exs new file mode 100644 index 00000000..40e28bcd --- /dev/null +++ b/priv/repo/migrations/20230217084253_create_application_device_activation.exs @@ -0,0 +1,16 @@ +defmodule Mobilizon.Repo.Migrations.CreateApplicationDeviceActivation do + use Ecto.Migration + + def change do + create table(:application_device_activation) do + add(:user_code, :string) + add(:device_code, :string) + add(:scope, :string) + add(:expires_in, :integer) + add(:status, :string, default: "pending") + add(:user_id, references(:users, on_delete: :delete_all), null: true) + add(:application_id, references(:applications, on_delete: :delete_all), null: false) + timestamps() + end + end +end diff --git a/test/mobilizon/applications_test.exs b/test/mobilizon/applications_test.exs index b1553393..01fbc41e 100644 --- a/test/mobilizon/applications_test.exs +++ b/test/mobilizon/applications_test.exs @@ -143,4 +143,78 @@ defmodule Mobilizon.ApplicationsTest do assert %Ecto.Changeset{} = Applications.change_application_token(application_token) end end + + describe "application_device_activation" do + alias Mobilizon.Applications.ApplicationDeviceActivation + + import Mobilizon.ApplicationsFixtures + + @invalid_attrs %{} + + test "list_application_device_activation/0 returns all application_device_activation" do + application_device_activation = application_device_activation_fixture() + assert Applications.list_application_device_activation() == [application_device_activation] + end + + test "get_application_device_activation!/1 returns the application_device_activation with given id" do + application_device_activation = application_device_activation_fixture() + + assert Applications.get_application_device_activation!(application_device_activation.id) == + application_device_activation + end + + test "create_application_device_activation/1 with valid data creates a application_device_activation" do + valid_attrs = %{} + + assert {:ok, %ApplicationDeviceActivation{} = application_device_activation} = + Applications.create_application_device_activation(valid_attrs) + end + + test "create_application_device_activation/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = + Applications.create_application_device_activation(@invalid_attrs) + end + + test "update_application_device_activation/2 with valid data updates the application_device_activation" do + application_device_activation = application_device_activation_fixture() + update_attrs = %{} + + assert {:ok, %ApplicationDeviceActivation{} = application_device_activation} = + Applications.update_application_device_activation( + application_device_activation, + update_attrs + ) + end + + test "update_application_device_activation/2 with invalid data returns error changeset" do + application_device_activation = application_device_activation_fixture() + + assert {:error, %Ecto.Changeset{}} = + Applications.update_application_device_activation( + application_device_activation, + @invalid_attrs + ) + + assert application_device_activation == + Applications.get_application_device_activation!(application_device_activation.id) + end + + test "delete_application_device_activation/1 deletes the application_device_activation" do + application_device_activation = application_device_activation_fixture() + + assert {:ok, %ApplicationDeviceActivation{}} = + Applications.delete_application_device_activation(application_device_activation) + + assert_raise Ecto.NoResultsError, fn -> + Applications.get_application_device_activation!(application_device_activation.id) + end + end + + test "change_application_device_activation/1 returns a application_device_activation changeset" do + application_device_activation = application_device_activation_fixture() + + assert %Ecto.Changeset{} = + Applications.change_application_device_activation(application_device_activation) + end + end end diff --git a/test/support/fixtures/applications_fixtures.ex b/test/support/fixtures/applications_fixtures.ex index 10f34109..0607e698 100644 --- a/test/support/fixtures/applications_fixtures.ex +++ b/test/support/fixtures/applications_fixtures.ex @@ -40,4 +40,16 @@ defmodule Mobilizon.ApplicationsFixtures do application_token end + + @doc """ + Generate a application_device_activation. + """ + def application_device_activation_fixture(attrs \\ %{}) do + {:ok, application_device_activation} = + attrs + |> Enum.into(%{}) + |> Mobilizon.Applications.create_application_device_activation() + + application_device_activation + end end