diff --git a/apps/api/lib/buildel/blocks/sharepoint_client.ex b/apps/api/lib/buildel/blocks/sharepoint_client.ex index c294b457..c42ffbe1 100644 --- a/apps/api/lib/buildel/blocks/sharepoint_client.ex +++ b/apps/api/lib/buildel/blocks/sharepoint_client.ex @@ -29,42 +29,49 @@ defmodule Buildel.Blocks.SharepointClient do "name" => name_schema(), "opts" => options_schema(%{ - "required" => ["client_secret", "client_id", "tenant_id"], - "properties" => %{ - "client_secret" => - secret_schema(%{ - "title" => "Client secret", - "description" => "Azure app client secret" - }), - "client_id" => %{ - "type" => "string", - "title" => "Client ID", - "description" => "Azure app client ID", - "default" => "", - "minLength" => 1 - }, - "tenant_id" => %{ - "type" => "string", - "title" => "Tenant ID", - "description" => "Azure app tenant ID", - "default" => "", - "minLength" => 1 - }, - "site_id" => %{ - "type" => "string", - "title" => "Site ID", - "description" => "SharePoint site ID", - "default" => "", - "minLength" => 1 - }, - "drive_id" => %{ - "type" => "string", - "title" => "Drive ID", - "description" => "SharePoint drive ID", - "default" => "", - "minLength" => 1 - } - } + "required" => ["client_secret", "client_id", "tenant_id", "site", "drive"], + "properties" => + Jason.OrderedObject.new( + client_secret: + secret_schema(%{ + "title" => "Client secret", + "description" => "Azure app client secret" + }), + client_id: %{ + "type" => "string", + "title" => "Client ID", + "description" => "Azure app client ID", + "default" => "", + "minLength" => 1 + }, + tenant_id: %{ + "type" => "string", + "title" => "Tenant ID", + "description" => "Azure app tenant ID", + "default" => "", + "minLength" => 1 + }, + site: %{ + "type" => "string", + "title" => "Site", + "description" => "SharePoint site ID", + "url" => + "/api/organizations/{{organization_id}}/tools/sharepoint/sites?client_id={{opts.client_id}}&secret_name={{opts.client_secret}}&tenant_id={{opts.tenant_id}}", + "presentAs" => "async-select", + "minLength" => 1, + "readonly" => true + }, + drive: %{ + "type" => "string", + "title" => "Drive", + "description" => "SharePoint drive ID", + "url" => + "/api/organizations/{{organization_id}}/tools/sharepoint/drives?client_id={{opts.client_id}}&secret_name={{opts.client_secret}}&tenant_id={{opts.tenant_id}}&site_id={{opts.site}}", + "presentAs" => "async-select", + "minLength" => 1, + "readonly" => true + } + ) }) } } @@ -86,8 +93,8 @@ defmodule Buildel.Blocks.SharepointClient do {:ok, state |> Map.put(:access_token, access_token) - |> Map.put(:site_id, opts.site_id) - |> Map.put(:drive_id, opts.drive_id)} + |> Map.put(:site_id, opts.site) + |> Map.put(:drive_id, opts.drive)} end defp get_access_token(client_id, client_secret, tenant_id) do diff --git a/apps/api/lib/buildel_web/controllers/organizations/tools/sharepoint_controller.ex b/apps/api/lib/buildel_web/controllers/organizations/tools/sharepoint_controller.ex new file mode 100644 index 00000000..0d46a87d --- /dev/null +++ b/apps/api/lib/buildel_web/controllers/organizations/tools/sharepoint_controller.ex @@ -0,0 +1,202 @@ +defmodule BuildelWeb.OrganizationToolSharepointController do + use BuildelWeb, :controller + use OpenApiSpex.ControllerSpecs + + import BuildelWeb.UserAuth + + alias Buildel.Organizations + + action_fallback(BuildelWeb.FallbackController) + + plug(:fetch_current_user) + plug(:require_authenticated_user) + + plug OpenApiSpex.Plug.CastAndValidate, + json_render_error_v2: true, + render_error: BuildelWeb.ErrorRendererPlug + + tags ["sharepoint"] + + operation :list_sites, + summary: "List sites", + parameters: [ + organization_id: [ + in: :path, + description: "Organization ID", + type: :integer, + required: true + ], + client_id: [ + in: :query, + description: "Client ID", + type: :string, + required: true + ], + secret_name: [ + in: :query, + description: "Client secret name", + type: :string, + required: true + ], + tenant_id: [ + in: :query, + description: "Tenant ID", + type: :string, + required: true + ] + ], + request_body: nil, + responses: [ + ok: {"organizations", "application/json", BuildelWeb.Schemas.Sharepoint.ListSitesResponse}, + unauthorized: + {"unauthorized", "application/json", BuildelWeb.Schemas.Errors.UnauthorizedResponse}, + forbidden: {"forbidden", "application/json", BuildelWeb.Schemas.Errors.ForbiddenResponse} + ], + security: [%{"authorization" => []}] + + def list_sites(conn, _) do + %{ + client_id: client_id, + secret_name: secret_name, + tenant_id: tenant_id, + organization_id: organization_id + } = conn.params + + user = conn.assigns.current_user + + with {:ok, organization} <- Organizations.get_user_organization(user, organization_id), + {:ok, %{value: client_secret}} <- + Buildel.Organizations.get_organization_secret(organization, secret_name), + {:ok, access_token} <- get_access_token(client_id, client_secret, tenant_id), + {:ok, sites} <- get_sites(access_token) do + render(conn, :list_sites, sites: sites["value"]) + end + end + + operation :list_drives, + summary: "List drives", + parameters: [ + organization_id: [ + in: :path, + description: "Organization ID", + type: :integer, + required: true + ], + client_id: [ + in: :query, + description: "Client ID", + type: :string, + required: true + ], + secret_name: [ + in: :query, + description: "Client secret name", + type: :string, + required: true + ], + tenant_id: [ + in: :query, + description: "Tenant ID", + type: :string, + required: true + ], + site_id: [ + in: :query, + description: "Site ID", + type: :string, + required: true + ] + ], + request_body: nil, + responses: [ + ok: {"organizations", "application/json", BuildelWeb.Schemas.Sharepoint.ListSitesResponse}, + unauthorized: + {"unauthorized", "application/json", BuildelWeb.Schemas.Errors.UnauthorizedResponse}, + forbidden: {"forbidden", "application/json", BuildelWeb.Schemas.Errors.ForbiddenResponse} + ], + security: [%{"authorization" => []}] + + def list_drives(conn, _) do + %{ + client_id: client_id, + secret_name: secret_name, + tenant_id: tenant_id, + organization_id: organization_id, + site_id: site_id + } = conn.params + + user = conn.assigns.current_user + + with {:ok, organization} <- Organizations.get_user_organization(user, organization_id), + {:ok, %{value: client_secret}} <- + Buildel.Organizations.get_organization_secret(organization, secret_name), + {:ok, access_token} <- get_access_token(client_id, client_secret, tenant_id), + {:ok, drives} <- get_drives(access_token, site_id) do + render(conn, :list_drives, drives: drives["value"]) + end + end + + defp get_sites(access_token) do + url = + "https://graph.microsoft.com/v1.0/sites?search=*" + + headers = [ + {"Authorization", "Bearer #{access_token}"} + ] + + case Req.new(url: url) |> Req.get(headers: headers) do + {:ok, %Req.Response{status: 200, body: body}} -> + {:ok, body} + + {:error, %Req.Response{body: reason}} -> + {:ok, reason} + + {:error, reason} -> + {:error, reason} + end + end + + defp get_drives(access_token, site_id) do + url = + "https://graph.microsoft.com/v1.0/sites/#{site_id}/drives" + + headers = [ + {"Authorization", "Bearer #{access_token}"} + ] + + case Req.new(url: url) |> Req.get(headers: headers) do + {:ok, %Req.Response{status: 200, body: body}} -> + {:ok, body} + + {:error, %Req.Response{body: reason}} -> + {:ok, reason} + + {:error, reason} -> + {:error, reason} + end + end + + defp get_access_token(client_id, client_secret, tenant_id) do + url = "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token" + + body = %{ + "client_id" => client_id, + "client_secret" => client_secret, + "scope" => "https://graph.microsoft.com/.default", + "grant_type" => "client_credentials" + } + + headers = [{"Content-Type", "application/x-www-form-urlencoded"}] + + case Req.new(url: url) |> Req.post(form: body, headers: headers) do + {:ok, %Req.Response{status: 200, body: body}} -> + {:ok, body["access_token"]} + + {:ok, %Req.Response{body: reason}} -> + {:error, reason} + + {:error, reason} -> + {:error, reason} + end + end +end diff --git a/apps/api/lib/buildel_web/controllers/organizations/tools/sharepoint_json.ex b/apps/api/lib/buildel_web/controllers/organizations/tools/sharepoint_json.ex new file mode 100644 index 00000000..231c6789 --- /dev/null +++ b/apps/api/lib/buildel_web/controllers/organizations/tools/sharepoint_json.ex @@ -0,0 +1,23 @@ +defmodule BuildelWeb.OrganizationToolSharepointJSON do + def list_sites(%{sites: sites}) do + %{data: for(site <- sites, do: site(site))} + end + + def list_drives(%{drives: drives}) do + %{data: for(drive <- drives, do: drive(drive))} + end + + defp drive(drive) do + %{ + id: drive["id"], + name: drive["name"] + } + end + + defp site(site) do + %{ + id: site["id"], + name: site["displayName"] + } + end +end diff --git a/apps/api/lib/buildel_web/router.ex b/apps/api/lib/buildel_web/router.ex index 394825f8..4581830f 100644 --- a/apps/api/lib/buildel_web/router.ex +++ b/apps/api/lib/buildel_web/router.ex @@ -365,6 +365,18 @@ defmodule BuildelWeb.Router do :create ) + get( + "/organizations/:organization_id/tools/sharepoint/sites", + OrganizationToolSharepointController, + :list_sites + ) + + get( + "/organizations/:organization_id/tools/sharepoint/drives", + OrganizationToolSharepointController, + :list_drives + ) + get("/users/me", UserController, :me) put("/users", UserController, :update) post("/users/log_in", UserSessionController, :create) diff --git a/apps/api/lib/buildel_web/schemas/organizations/tools/sharepoint.ex b/apps/api/lib/buildel_web/schemas/organizations/tools/sharepoint.ex new file mode 100644 index 00000000..bd0365e4 --- /dev/null +++ b/apps/api/lib/buildel_web/schemas/organizations/tools/sharepoint.ex @@ -0,0 +1,31 @@ +defmodule BuildelWeb.Schemas.Sharepoint do + alias OpenApiSpex.Schema + + defmodule ListSitesResponse do + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "SharepointListSitesResponse", + type: :object, + properties: %{ + id: %Schema{type: :string, description: "Site ID"}, + name: %Schema{type: :string, description: "Site name"} + }, + required: [:id, :name] + }) + end + + defmodule ListDrivesResponse do + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "SharepointListDrivesResponse", + type: :object, + properties: %{ + id: %Schema{type: :string, description: "Drive ID"}, + name: %Schema{type: :string, description: "Drive name"} + }, + required: [:id, :name] + }) + end +end