diff --git a/backend/lib/azimutt/accounts.ex b/backend/lib/azimutt/accounts.ex index da076cce2..6085d3409 100644 --- a/backend/lib/azimutt/accounts.ex +++ b/backend/lib/azimutt/accounts.ex @@ -345,7 +345,7 @@ defmodule Azimutt.Accounts do if Azimutt.config(:global_organization) && Azimutt.config(:global_organization_alone) do user.organizations |> Enum.filter(fn orga -> orga.id == Azimutt.config(:global_organization) end) else - user.organizations + user.organizations |> Enum.filter(fn orga -> orga.deleted_at == nil end) end end diff --git a/backend/lib/azimutt_web/controllers/api/clever_cloud_controller.ex b/backend/lib/azimutt_web/controllers/api/clever_cloud_controller.ex index 767197752..3095a46fa 100644 --- a/backend/lib/azimutt_web/controllers/api/clever_cloud_controller.ex +++ b/backend/lib/azimutt_web/controllers/api/clever_cloud_controller.ex @@ -3,13 +3,10 @@ defmodule AzimuttWeb.Api.CleverCloudController do use AzimuttWeb, :controller require Logger alias Azimutt.CleverCloud - alias Azimutt.Utils.Stringx action_fallback AzimuttWeb.Api.FallbackController # https://www.clever-cloud.com/doc/extend/add-ons-api/#provisioning def create(conn, params) do - Logger.info("Api.CleverCloudController.create(#{Stringx.inspect(params)})") - case CleverCloud.create_resource(params) do {:ok, resource} -> conn |> render("show.json", resource: resource, message: "Your Azimutt add-on is now provisioned.") {:error, _err} -> conn |> send_resp(:unprocessable_entity, "") @@ -17,8 +14,7 @@ defmodule AzimuttWeb.Api.CleverCloudController do end # https://www.clever-cloud.com/doc/extend/add-ons-api/#plan-change - def update(conn, %{"resource_id" => resource_id, "plan" => plan} = params) do - Logger.info("Api.CleverCloudController.update(#{Stringx.inspect(params)})") + def update(conn, %{"resource_id" => resource_id, "plan" => plan}) do now = DateTime.utc_now() case CleverCloud.get_resource(resource_id) do @@ -37,8 +33,7 @@ defmodule AzimuttWeb.Api.CleverCloudController do end # https://www.clever-cloud.com/doc/extend/add-ons-api/#deprovisioning - def delete(conn, %{"resource_id" => resource_id} = params) do - Logger.info("Api.CleverCloudController.delete(#{Stringx.inspect(params)})") + def delete(conn, %{"resource_id" => resource_id}) do now = DateTime.utc_now() case CleverCloud.get_resource(resource_id) do diff --git a/backend/lib/azimutt_web/controllers/clever_cloud_controller.ex b/backend/lib/azimutt_web/controllers/clever_cloud_controller.ex index 4bcb359e6..dba0475f0 100644 --- a/backend/lib/azimutt_web/controllers/clever_cloud_controller.ex +++ b/backend/lib/azimutt_web/controllers/clever_cloud_controller.ex @@ -12,8 +12,7 @@ defmodule AzimuttWeb.CleverCloudController do action_fallback AzimuttWeb.FallbackController # helper to ease clever cloud testing in local - def index(conn, params) do - Logger.info("CleverCloudController.index(#{Stringx.inspect(params)})") + def index(conn, _params) do # defined as env variable (see .env), don't use env vars to make leak impossible clever_cloud = %{ addon_id: "azimutt-dev", @@ -28,7 +27,6 @@ defmodule AzimuttWeb.CleverCloudController do # https://www.clever-cloud.com/doc/extend/add-ons-api/#sso # TODO: how to get user_id in SSO? Get it from the resource? What happen if several users from Clever Cloud??? def login(conn, %{"id" => resource_id, "token" => token, "timestamp" => timestamp, "email" => email} = params) do - Logger.info("CleverCloudController.login(#{Stringx.inspect(params)})") now = DateTime.utc_now() now_ts = System.os_time(:second) salt = Azimutt.config(:clever_cloud_sso_salt) @@ -91,8 +89,7 @@ defmodule AzimuttWeb.CleverCloudController do end end - def show(conn, %{"resource_id" => resource_id} = params) do - Logger.info("CleverCloudController.show(#{Stringx.inspect(params)})") + def show(conn, %{"resource_id" => resource_id}) do current_user = conn.assigns.current_user resource = conn.assigns.clever_cloud diff --git a/backend/lib/azimutt_web/controllers/user_auth.ex b/backend/lib/azimutt_web/controllers/user_auth.ex index 4f7159237..a5db781b2 100644 --- a/backend/lib/azimutt_web/controllers/user_auth.ex +++ b/backend/lib/azimutt_web/controllers/user_auth.ex @@ -9,7 +9,6 @@ defmodule AzimuttWeb.UserAuth do alias Azimutt.Heroku alias Azimutt.Tracking alias Azimutt.Utils.Result - alias Azimutt.Utils.Stringx alias AzimuttWeb.Router.Helpers, as: Routes @seconds 1 @@ -25,7 +24,7 @@ defmodule AzimuttWeb.UserAuth do # cf https://www.clever-cloud.com/doc/extend/add-ons-api/#sso @clever_cloud_cookie "_azimutt_clever_cloud_sso" - @clever_cloud_options [sign: true, max_age: 90 * @minutes, same_site: "Lax"] + @clever_cloud_options [sign: true, max_age: 90 * @minutes, same_site: "None", secure: true] # cf https://devcenter.heroku.com/articles/add-on-single-sign-on @heroku_cookie "_azimutt_heroku_sso" @@ -211,8 +210,6 @@ defmodule AzimuttWeb.UserAuth do do: require_basic_auth(conn, "Heroku", Azimutt.config(:heroku_addon_id), Azimutt.config(:heroku_password)) defp require_basic_auth(conn, name, expected_user, expected_pass) do - Logger.info("UserAuth.require_basic_auth(#{Stringx.inspect(%{name: name, user: expected_user, pass: expected_pass})})") - if expected_user && expected_pass do case Plug.BasicAuth.parse_basic_auth(conn) do {user, pass} -> @@ -269,8 +266,6 @@ defmodule AzimuttWeb.UserAuth do end def require_clever_cloud_resource(conn, _opts) do - Logger.info("UserAuth.require_clever_cloud_resource()") - if conn.assigns[:clever_cloud] do conn else diff --git a/backend/lib/azimutt_web/router.ex b/backend/lib/azimutt_web/router.ex index 3a5440c57..120a9171f 100644 --- a/backend/lib/azimutt_web/router.ex +++ b/backend/lib/azimutt_web/router.ex @@ -194,7 +194,7 @@ defmodule AzimuttWeb.Router do end scope "/clevercloud", AzimuttWeb do - pipe_through([:browser, :require_clever_cloud_resource, :require_authed_user]) + pipe_through([:browser, :require_clever_cloud_resource, :require_authed_user, AllowCrossOriginIframe]) get("/resources/:resource_id", CleverCloudController, :show) end @@ -306,8 +306,13 @@ defmodule AzimuttWeb.Router do get("/create", ElmController, :create) get("/new", ElmController, :new) get("/:organization_id", ElmController, :orga_show) - get("/:organization_id/create", ElmController, :orga_create) get("/:organization_id/new", ElmController, :orga_new) + end + + # allow cross origin iframe for Clever Cloud + scope "/", AzimuttWeb do + pipe_through([:browser, :enforce_user_requirements, :elm_root_layout, AllowCrossOriginIframe]) + get("/:organization_id/create", ElmController, :orga_create) get("/:organization_id/:project_id", ElmController, :project_show) end end diff --git a/backend/lib/azimutt_web/templates/website/index.html.heex b/backend/lib/azimutt_web/templates/website/index.html.heex index e48e81c1d..c352f085c 100644 --- a/backend/lib/azimutt_web/templates/website/index.html.heex +++ b/backend/lib/azimutt_web/templates/website/index.html.heex @@ -3,29 +3,20 @@
-
- <%= if @conn.query_params["ref"] == "producthunt" do %> -

- -

- <% else %> -

- - See our launch -

- <% end %> -
-
-
-

- Boost your database productivity. Right now! -

-
- - - 611 stars - - +
+ Azimutt - Easily explore and analyze your database with your team | Product Hunt +
+
+
+

+ Boost your database productivity. Right now! +

+ @@ -186,4 +177,4 @@
<%= render "_footer.html", conn: @conn %> -<%= render "_heroku_addon.html" %> +<%= # render "_heroku_addon.html" %> diff --git a/frontend/src/Models/Project/TableId.elm b/frontend/src/Models/Project/TableId.elm index 3077e7f96..ee2abc751 100644 --- a/frontend/src/Models/Project/TableId.elm +++ b/frontend/src/Models/Project/TableId.elm @@ -51,8 +51,8 @@ toString ( s, t ) = fromString : String -> Maybe TableId fromString id = case String.split "." id of - s :: t :: [] -> - Just ( s, t ) + s :: t :: rest -> + Just ( s, (t :: rest) |> String.join "." ) _ -> Nothing diff --git a/frontend/src/PagesComponents/Organization_/Project_/Views/Navbar.elm b/frontend/src/PagesComponents/Organization_/Project_/Views/Navbar.elm index 2571f1561..7e15e2b92 100644 --- a/frontend/src/PagesComponents/Organization_/Project_/Views/Navbar.elm +++ b/frontend/src/PagesComponents/Organization_/Project_/Views/Navbar.elm @@ -82,12 +82,16 @@ viewNavbar gConf maybeUser eConf virtualRelation erd projects model args = , Just { action = Left Conf.constants.azimuttFeatureRequests, content = text "Suggest a feature 🚀", hotkeys = [] } ] |> List.filterMap identity + + notCleverCloud : Bool + notCleverCloud = + (erd.project.organization |> Maybe.andThen .cleverCloud) /= Nothing in nav [ css [ "az-navbar relative z-max bg-primary-600" ] ] [ div [ css [ "mx-auto px-2", sm [ "px-4" ], lg [ "px-8" ] ] ] [ div [ class "relative flex items-center justify-between h-16" ] [ div [ css [ "flex items-center px-2", lg [ "px-0" ] ] ] - [ viewNavbarBrand (erd.project.organization |> Maybe.map .id |> Maybe.orElse urlOrganization |> Maybe.filter (\id -> userOrganizations |> List.member id)) eConf + [ viewNavbarBrand (erd.project.organization |> Maybe.map .id |> Maybe.orElse urlOrganization |> Maybe.filter (\id -> userOrganizations |> List.member id)) (eConf.dashboardLink && notCleverCloud) , Lazy.lazy8 viewNavbarSearch erd.settings.defaultSchema model.search erd.tables erd.relations erd.metadata (erd |> Erd.currentLayout |> .tables) (htmlId ++ "-search") (openedDropdown |> String.filterStartsWith (htmlId ++ "-search")) , viewNavbarHelp ] @@ -109,12 +113,12 @@ viewNavbar gConf maybeUser eConf virtualRelation erd projects model args = ] -viewNavbarBrand : Maybe OrganizationId -> ErdConf -> Html msg -viewNavbarBrand organization conf = +viewNavbarBrand : Maybe OrganizationId -> Bool -> Html msg +viewNavbarBrand organization dashboardLink = let attrs : List (Attribute msg) attrs = - if conf.dashboardLink then + if dashboardLink then [ href (organization |> Backend.organizationUrl) ] else diff --git a/frontend/tests/Models/Project/ColumnTest.elm b/frontend/tests/Models/Project/ColumnTest.elm index ce22389bf..0baeeaf96 100644 --- a/frontend/tests/Models/Project/ColumnTest.elm +++ b/frontend/tests/Models/Project/ColumnTest.elm @@ -1,26 +1,27 @@ module Models.Project.ColumnTest exposing (..) -import Json.Decode as Decode import Libs.Ned as Ned exposing (Ned) import Libs.Nel as Nel exposing (Nel) import Models.Project.Column as Column exposing (Column, NestedColumns(..)) import Models.Project.Comment exposing (Comment) import Test exposing (Test, describe) -import TestHelpers.JsonTest exposing (jsonTest) +import TestHelpers.Helpers exposing (testEncode) suite : Test suite = describe "Column" [ describe "serialization" - [ testSerialization "basic" - """{"name":"id","type":"int"}""" + [ testEncode "basic" + Column.encode (Column 1 "id" "int" False Nothing Nothing Nothing []) - , testSerialization "full" - """{"name":"id","type":"int","nullable":true,"default":"0","comment":{"text":"id col"}}""" + """{"name":"id","type":"int"}""" + , testEncode "full" + Column.encode (Column 1 "id" "int" True (Just "0") (Just (Comment "id col" [])) Nothing []) - , testSerialization "nested" - """{"name":"id","type":"Object","columns":[{"name":"kind","type":"string"},{"name":"value","type":"number"}]}""" + """{"name":"id","type":"int","nullable":true,"default":"0","comment":{"text":"id col"}}""" + , testEncode "nested" + Column.encode (Column 1 "id" "Object" @@ -35,10 +36,6 @@ suite = ) [] ) + """{"name":"id","type":"Object","columns":[{"name":"kind","type":"string"},{"name":"value","type":"number"}]}""" ] ] - - -testSerialization : String -> String -> Column -> Test -testSerialization name json column = - jsonTest ("Column " ++ name) column json Column.encode (Column.decode |> Decode.map (\f -> f 1)) diff --git a/frontend/tests/Models/Project/LayoutTest.elm b/frontend/tests/Models/Project/LayoutTest.elm new file mode 100644 index 000000000..894c503de --- /dev/null +++ b/frontend/tests/Models/Project/LayoutTest.elm @@ -0,0 +1,33 @@ +module Models.Project.LayoutTest exposing (..) + +import Libs.Tailwind as Tw +import Libs.Time as Time +import Models.Position as Position +import Models.Project.Group exposing (Group) +import Models.Project.Layout as Layout exposing (Layout) +import Models.Project.TableProps exposing (TableProps) +import Models.Size as Size +import PagesComponents.Organization_.Project_.Models.Memo exposing (Memo) +import Test exposing (Test, describe) +import TestHelpers.Helpers exposing (testEncode) + + +suite : Test +suite = + describe "Layout" + [ describe "serde" + [ testEncode "empty" Layout.encode (Layout [] [] [] Time.zero Time.zero) """{"tables":[],"createdAt":0,"updatedAt":0}""" + , testEncode "empty table" + Layout.encode + (Layout [ TableProps ( "", "users" ) Position.zeroGrid Size.zeroCanvas Tw.gray [] False False False ] [] [] Time.zero Time.zero) + """{"tables":[{"id":".users","position":{"left":0,"top":0},"size":{"width":0,"height":0},"color":"gray","columns":[]}],"createdAt":0,"updatedAt":0}""" + , testEncode "empty group" + Layout.encode + (Layout [] [ Group "group 1" [ ( "", "users" ) ] Tw.gray False ] [] Time.zero Time.zero) + """{"tables":[],"groups":[{"name":"group 1","tables":[".users"],"color":"gray"}],"createdAt":0,"updatedAt":0}""" + , testEncode "empty memo" + Layout.encode + (Layout [] [] [ Memo 1 "hey" Position.zeroGrid Size.zeroCanvas Nothing ] Time.zero Time.zero) + """{"tables":[],"memos":[{"id":1,"content":"hey","position":{"left":0,"top":0},"size":{"width":0,"height":0}}],"createdAt":0,"updatedAt":0}""" + ] + ] diff --git a/frontend/tests/Models/Project/TableIdTest.elm b/frontend/tests/Models/Project/TableIdTest.elm index de27931fe..ff34bfa7c 100644 --- a/frontend/tests/Models/Project/TableIdTest.elm +++ b/frontend/tests/Models/Project/TableIdTest.elm @@ -4,6 +4,7 @@ import Expect import Models.Project.SchemaName exposing (SchemaName) import Models.Project.TableId as TableId exposing (TableId) import Test exposing (Test, describe, test) +import TestHelpers.Helpers exposing (testEncode, testSerde) defaultSchema : SchemaName @@ -48,4 +49,14 @@ suite = , test "with empty schema" (\_ -> ".users" |> TableId.parse |> Expect.equal tableId3) , test "with no schema" (\_ -> "users" |> TableId.parse |> Expect.equal tableId3) ] + , describe "encode" + [ testEncode "empty schema" TableId.encode ( "", "users" ) "\".users\"" + , testEncode "with schema" TableId.encode ( "public", "users" ) "\"public.users\"" + , testEncode "name with dot" TableId.encode ( "", "users.aa" ) "\".users.aa\"" + ] + , describe "serde" + [ testSerde "empty schema" TableId.encode TableId.decode ( "", "users" ) + , testSerde "with schema" TableId.encode TableId.decode ( "public", "users" ) + , testSerde "name with dot" TableId.encode TableId.decode ( "", "users.aa" ) + ] ] diff --git a/frontend/tests/TestHelpers/Helpers.elm b/frontend/tests/TestHelpers/Helpers.elm new file mode 100644 index 000000000..025701cea --- /dev/null +++ b/frontend/tests/TestHelpers/Helpers.elm @@ -0,0 +1,16 @@ +module TestHelpers.Helpers exposing (..) + +import Expect +import Json.Decode as Decode +import Json.Encode as Encode +import Test exposing (Test, test) + + +testSerde : String -> (a -> Encode.Value) -> Decode.Decoder a -> a -> Test +testSerde name encode decode value = + test name (\_ -> value |> encode |> Encode.encode 0 |> Decode.decodeString decode |> Expect.equal (Ok value)) + + +testEncode : String -> (a -> Encode.Value) -> a -> String -> Test +testEncode name encode value json = + test name (\_ -> value |> encode |> Encode.encode 0 |> Expect.equal json) diff --git a/frontend/ts-src/services/backend.ts b/frontend/ts-src/services/backend.ts index 5cdf9d4b1..94fdae84e 100644 --- a/frontend/ts-src/services/backend.ts +++ b/frontend/ts-src/services/backend.ts @@ -173,7 +173,7 @@ export class Backend { private gatewayUrl = async (): Promise => { const cliGateway = 'http://localhost:4177' - return await fetch(`${cliGateway}/ping`).then(_ => `${cliGateway}/gateway`).catch(_ => `${window.gateway_url}/gateway`) + return await fetch(`${cliGateway}/ping`, {cache: 'no-store'}).then(_ => `${cliGateway}/gateway`).catch(_ => `${window.gateway_url}/gateway`) } }