diff --git a/.gitignore b/.gitignore index c5b7809..152ffe5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # The directory Mix will write compiled artifacts to. /_build/ +# Gleam build dir +/build/ + # If you run "mix test --cover", coverage assets end up here. /cover/ diff --git a/.tool-versions b/.tool-versions index 4cca4f7..4ac13b4 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ erlang 25.0.4 elixir 1.13.4-otp-25 +gleam 0.25.3 diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..4683f3c --- /dev/null +++ b/gleam.toml @@ -0,0 +1,10 @@ +name = "geo_therminator" +version = "0.3.0" +target = "erlang" + +[dependencies] +gleam_stdlib = "~> 0.25" +gleam_http = "~> 3.1" +gleam_hackney = "~> 0.2.1" +gleam_erlang = "~> 0.17.1" +gleam_json = "~> 0.5.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..6cfed50 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,26 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "certifi", version = "2.9.0", build_tools = ["rebar3"], requirements = [], otp_app = "certifi", source = "hex", outer_checksum = "266DA46BDB06D6C6D35FDE799BCB28D36D985D424AD7C08B5BB48F5B5CDD4641" }, + { name = "gleam_erlang", version = "0.17.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "BAAA84F5BCC4477E809BA3E03BB3009A3894A6544C1511626C44408E39DB2AE6" }, + { name = "gleam_hackney", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "hackney", "gleam_http"], otp_app = "gleam_hackney", source = "hex", outer_checksum = "CCACA00027C827436D8EB945651392B6E5798CFC9E69907A28BE61832B0C02A4" }, + { name = "gleam_http", version = "3.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "B66B7A1539CCB577119E4DC80DD3484C1A652CB032967954498EEDBAE3355763" }, + { name = "gleam_json", version = "0.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "E42443C98AA66E30143C24818F2CEA801491C10CE6B1A5EDDF3FC4ABDC7601CB" }, + { name = "gleam_stdlib", version = "0.25.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "AD0F89928E0B919C8F8EDF640484633B28DBF88630A9E6AE504617A3E3E5B9A2" }, + { name = "hackney", version = "1.18.1", build_tools = ["rebar3"], requirements = ["certifi", "mimerl", "parse_trans", "ssl_verify_fun", "unicode_util_compat", "metrics", "idna"], otp_app = "hackney", source = "hex", outer_checksum = "A4ECDAFF44297E9B5894AE499E9A070EA1888C84AFDD1FD9B7B2BC384950128E" }, + { name = "idna", version = "6.1.1", build_tools = ["rebar3"], requirements = ["unicode_util_compat"], otp_app = "idna", source = "hex", outer_checksum = "92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA" }, + { name = "metrics", version = "1.0.1", build_tools = ["rebar3"], requirements = [], otp_app = "metrics", source = "hex", outer_checksum = "69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16" }, + { name = "mimerl", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "mimerl", source = "hex", outer_checksum = "F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323" }, + { name = "parse_trans", version = "3.3.1", build_tools = ["rebar3"], requirements = [], otp_app = "parse_trans", source = "hex", outer_checksum = "07CD9577885F56362D414E8C4C4E6BDF10D43A8767ABB92D24CBE8B24C54888B" }, + { name = "ssl_verify_fun", version = "1.1.6", build_tools = ["mix", "rebar3", "make"], requirements = [], otp_app = "ssl_verify_fun", source = "hex", outer_checksum = "BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680" }, + { name = "thoas", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "442296847ACA11DB8D25180693D7CA3073D6D7179F66952F07B16415306513B6" }, + { name = "unicode_util_compat", version = "0.7.0", build_tools = ["rebar3"], requirements = [], otp_app = "unicode_util_compat", source = "hex", outer_checksum = "25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521" }, +] + +[requirements] +gleam_erlang = "~> 0.17.1" +gleam_hackney = "~> 0.2.1" +gleam_http = "~> 3.1" +gleam_json = "~> 0.5.0" +gleam_stdlib = "~> 0.25" diff --git a/mix.exs b/mix.exs index 793109b..971d41b 100644 --- a/mix.exs +++ b/mix.exs @@ -1,13 +1,20 @@ defmodule GeoTherminator.MixProject do use Mix.Project + @app :geo_therminator + def project do [ - app: :geo_therminator, - version: "0.2.0", + app: @app, + version: "0.3.0", elixir: "~> 1.13", elixirc_paths: elixirc_paths(Mix.env()), - compilers: [:gettext] ++ Mix.compilers(), + erlc_paths: [ + "build/dev/erlang/#{@app}/_gleam_artefacts" + ], + erlc_include_path: "build/dev/erlang/#{@app}/include", + archives: [mix_gleam: "~> 0.6.1"], + compilers: [:gleam, :gettext] ++ Mix.compilers(), start_permanent: Mix.env() == :prod, aliases: aliases(), deps: deps(), @@ -54,7 +61,12 @@ defmodule GeoTherminator.MixProject do {:dotenv_parser, "~> 1.2"}, {:finch, "~> 0.9.0"}, {:desktop, "~> 1.4"}, - {:cubdb, "~> 2.0"} + {:cubdb, "~> 2.0"}, + {:gleam_stdlib, "~> 0.25"}, + {:gleam_http, "~> 3.1"}, + {:gleam_hackney, "~> 0.2.1"}, + {:gleam_erlang, "~> 0.17.1"}, + {:gleam_json, "~> 0.5.0"} ] if Mix.target() in [:android, :ios] do @@ -73,7 +85,8 @@ defmodule GeoTherminator.MixProject do defp aliases do [ setup: ["deps.get"], - "assets.deploy": ["esbuild default --minify", "phx.digest"] + "assets.deploy": ["esbuild default --minify", "phx.digest"], + "deps.get": ["deps.get", "gleam.deps.get"] ] end end diff --git a/mix.lock b/mix.lock index 14ee227..2424284 100644 --- a/mix.lock +++ b/mix.lock @@ -1,42 +1,56 @@ %{ - "castore": {:hex, :castore, "0.1.13", "ccf3ab251ffaebc4319f41d788ce59a6ab3f42b6c27e598ad838ffecee0b04f9", [:mix], [], "hexpm", "a14a7eecfec7e20385493dbb92b0d12c5d77ecfd6307de10102d58c94e8c49c0"}, + "castore": {:hex, :castore, "0.1.20", "62a0126cbb7cb3e259257827b9190f88316eb7aa3fdac01fd6f2dfd64e7f46e9", [:mix], [], "hexpm", "a020b7650529c986c454a4035b6b13a328e288466986307bea3aadb4c95ac98a"}, + "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, - "cubdb": {:hex, :cubdb, "2.0.1", "24cab8fb4128df704c52ed641f5ed70af352f7a3a80cebbb44c3bbadc3fd5f45", [:mix], [], "hexpm", "57cf25aebfc34f4580d9075da06882b4fe3e0739f5353d4dcc213e9cc1b10cdf"}, + "cubdb": {:hex, :cubdb, "2.0.2", "d4253885084dae37a8ff73887d232864eb38ecac962aa08543e686b0183a1d62", [:mix], [], "hexpm", "c99cc8f9e6c4deb98d16cca5ded1928edd22e48b4736b76e8a1a85367d7fe921"}, "dbus": {:hex, :dbus, "0.8.0", "7c800681f35d909c199265e55a8ee4aea9ebe4acccce77a0740f89f29cc57648", [:make], [], "hexpm", "a9784f2d9717ffa1f74169144a226c39633ac0d9c7fe8cb3594aeb89c827cca5"}, "debouncer": {:hex, :debouncer, "0.1.7", "a7f59fb55cdb54072aff8ece461f4d041d2a709da84e07ed0ab302d348724640", [:mix], [], "hexpm", "b7fd0623df8ab16933bb164d19769884b18c98cab8677cd53eed59587f290603"}, - "desktop": {:hex, :desktop, "1.4.1", "69e4741fbf72e2e71c8d6078cd18da0e6fddc6e87f67837ac501a2412a0ddc64", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_sni, "~> 0.2", [hex: :ex_sni, repo: "hexpm", optional: false]}, {:gettext, "> 0.10.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:oncrash, "~> 0.1", [hex: :oncrash, repo: "hexpm", optional: false]}, {:phoenix, "> 1.0.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "> 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "fd3716fd912d2379448d94e445ce9de6e65e89cd2bd9e35f8d76efd497f1ec66"}, + "desktop": {:hex, :desktop, "1.4.2", "48cb5f02aa77522bd9996bfe02c4b23f8dc40d30076ada46b660f4a20bd7a3a1", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_sni, "~> 0.2", [hex: :ex_sni, repo: "hexpm", optional: false]}, {:gettext, "> 0.10.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:oncrash, "~> 0.1", [hex: :oncrash, repo: "hexpm", optional: false]}, {:phoenix, "> 1.0.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "> 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bcaee5daf0c547ed988d26d4bec04388f104ce169a4e255a786cd64598ce3362"}, "dotenv_parser": {:hex, :dotenv_parser, "1.2.0", "f062900aeb57727b619aeb182fa4a8b1cbb7b4260ebec2b70b3d5c064885aff3", [:mix], [], "hexpm", "eddd69e7fde28618adb2e4153fa380db5c56161b32341e7a4e0530d86987c47f"}, - "esbuild": {:hex, :esbuild, "0.3.4", "416203c642eb84b207f882cf7953a1fd7bb71e23f5f86554f983bb7bad18b897", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c472e38b37e9547113776b1e4b64b44ec540bcc7056dd252c2c3ffba41aa9793"}, + "esbuild": {:hex, :esbuild, "0.6.0", "9ba6ead054abd43cb3d7b14946a0cdd1493698ccd8e054e0e5d6286d7f0f509c", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "30f9a05d4a5bab0d3e37398f312f80864e1ee1a081ca09149d06d474318fd040"}, "ex_dbus": {:hex, :ex_dbus, "0.1.4", "053df83d45b27ba0b9b6ef55a47253922069a3ace12a2a7dd30d3aff58301e17", [:mix], [{:dbus, "~> 0.8.0", [hex: :dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "d8baeaf465eab57b70a47b70e29fdfef6eb09ba110fc37176eebe6ac7874d6d5"}, "ex_sni": {:hex, :ex_sni, "0.2.9", "81f9421035dd3edb6d69f1a4dd5f53c7071b41628130d32ba5ab7bb4bfdc2da0", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_dbus, "~> 0.1", [hex: :ex_dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "921d67d913765ed20ea8354fd1798dabc957bf66990a6842d6aaa7cd5ee5bc06"}, + "expo": {:hex, :expo, "0.1.0", "d4e932bdad052c374118e312e35280f1919ac13881cb3ac07a209a54d0c81dd8", [:mix], [], "hexpm", "c22c536021c56de058aaeedeabb4744eb5d48137bacf8c29f04d25b6c6bbbf45"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "finch": {:hex, :finch, "0.9.0", "8b772324aebafcaba763f1dffaa3e7f52f8c4e52485f50f48bbb2f42219a2e87", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a93bfcad9ca50fa3cb2d459f27667d9a87cfbb7fecf9b29b2e78a50bc2ab445d"}, - "floki": {:hex, :floki, "0.32.0", "f915dc15258bc997d49be1f5ef7d3992f8834d6f5695270acad17b41f5bcc8e2", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "1c5a91cae1fd8931c26a4826b5e2372c284813904c8bacb468b5de39c7ececbd"}, - "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"}, - "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, - "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, - "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, - "mint": {:hex, :mint, "1.4.0", "cd7d2451b201fc8e4a8fd86257fb3878d9e3752899eb67b0c5b25b180bde1212", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "10a99e144b815cbf8522dccbc8199d15802440fc7a64d67b6853adb6fa170217"}, - "nimble_options": {:hex, :nimble_options, "0.3.7", "1e52dd7673d36138b1a5dede183b5d86dff175dc46d104a8e98e396b85b04670", [:mix], [], "hexpm", "2086907e6665c6b6579be54ef5001928df5231f355f71ed258f80a55e9f63633"}, - "nimble_pool": {:hex, :nimble_pool, "0.2.4", "1db8e9f8a53d967d595e0b32a17030cdb6c0dc4a451b8ac787bf601d3f7704c3", [:mix], [], "hexpm", "367e8071e137b787764e6a9992ccb57b276dc2282535f767a07d881951ebeac6"}, + "finch": {:hex, :finch, "0.9.1", "ab2b0151ba88543e221cb50bf0734860db55e8748816ee16e4997fe205f7b315", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6d6b898a59d19f84958eaffec40580f5a9ff88a31e93156707fa8b1d552aa425"}, + "floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"}, + "gettext": {:hex, :gettext, "0.21.0", "15bbceb20b317b706a8041061a08e858b5a189654128618b53746bf36c84352b", [:mix], [{:expo, "~> 0.1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "04a66db4103b6d1d18f92240bb2c73167b517229316b7bef84e4eebbfb2f14f6"}, + "gleam_erlang": {:hex, :gleam_erlang, "0.17.1", "40fff501e8ca39fa166f4c12ed13bb57e94fc5bb59a93b4446687d82d4a12ff9", [:gleam], [{:gleam_stdlib, "~> 0.22", [hex: :gleam_stdlib, repo: "hexpm", optional: false]}], "hexpm", "baaa84f5bcc4477e809ba3e03bb3009a3894a6544c1511626c44408e39db2ae6"}, + "gleam_hackney": {:hex, :gleam_hackney, "0.2.1", "ca3c5677b85f31885a4366c73a110803515d6d23a2e233e459dc164260315404", [:gleam], [{:gleam_http, "~> 3.0", [hex: :gleam_http, repo: "hexpm", optional: false]}, {:gleam_stdlib, "~> 0.18", [hex: :gleam_stdlib, repo: "hexpm", optional: false]}, {:hackney, "~> 1.18", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "ccaca00027c827436d8eb945651392b6e5798cfc9e69907a28be61832b0c02a4"}, + "gleam_http": {:hex, :gleam_http, "3.1.1", "609158240630e21fc70c69b21384e5ebbcd86f71bd378a6f7c2b87f910ab3561", [:gleam], [{:gleam_stdlib, "~> 0.18", [hex: :gleam_stdlib, repo: "hexpm", optional: false]}], "hexpm", "b66b7a1539ccb577119e4dc80dd3484c1a652cb032967954498eedbae3355763"}, + "gleam_json": {:hex, :gleam_json, "0.5.0", "aff4507ad7700ad794ada6671c6dfd0174696713659bd8782858135b19f41b58", [:gleam], [{:gleam_stdlib, "~> 0.19", [hex: :gleam_stdlib, repo: "hexpm", optional: false]}, {:thoas, "~> 0.2", [hex: :thoas, repo: "hexpm", optional: false]}], "hexpm", "e42443c98aa66e30143c24818f2cea801491c10ce6b1a5eddf3fc4abdc7601cb"}, + "gleam_stdlib": {:hex, :gleam_stdlib, "0.25.0", "656f39258dcc8772719e463bbe7d1d1c7800238a520b41558fad53ea206ee3ab", [:gleam], [], "hexpm", "ad0f89928e0b919c8f8edf640484633b28dbf88630a9e6ae504617a3e3e5b9a2"}, + "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, + "nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"}, + "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, "oncrash": {:hex, :oncrash, "0.1.0", "9cf4ae8eba4ea250b579470172c5e9b8c75418b2264de7dbcf42e408d62e30fb", [:mix], [], "hexpm", "6968e775491cd857f9b6ff940bf2574fd1c2fab84fa7e14d5f56c39174c00018"}, - "phoenix": {:hex, :phoenix, "1.6.12", "f8f8ac077600f84419806dd53114b2e77aedde7a502e74181a7d886355aa0643", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d6cf5583c9c20f7103c40e6014ef802d96553b8e5d6585ad6e627bd5ddb0d12"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"}, "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.1", "fb94a33c077141f9ac7930b322a7a3b99f9b144bf3a08dd667b9f9aaf0319889", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "6faf1373e5846c8ab68c2cf55cfa5c196c1fbbe0c72d12a4cdfaaac6ef189948"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.11", "205f6aa5405648c76f2abcd57716f42fc07d8f21dd8ea7b262dd12b324b50c95", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7177791944b7f90ed18f5935a6a5f07f760b36f7b3bdfb9d28c57440a3c43f99"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.12", "74f4c0ad02d7deac2d04f50b52827a5efdc5c6e7fac5cede145f5f0e4183aedc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "af6dd5e0aac16ff43571f527a8e0616d62cb80b10eb87aac82170243e50d99c8"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, - "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, - "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, + "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"}, "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, - "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "saxy": {:hex, :saxy, "1.4.0", "c7203ad20001f72eaaad07d08f82be063fa94a40924e6bb39d93d55f979abcba", [:mix], [], "hexpm", "3fe790354d3f2234ad0b5be2d99822a23fa2d4e8ccd6657c672901dac172e9a9"}, - "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "telemetry": {:hex, :telemetry, "1.2.0", "a8ce551485a9a3dac8d523542de130eafd12e40bbf76cf0ecd2528f24e812a44", [:rebar3], [], "hexpm", "1427e73667b9a2002cf1f26694c422d5c905df889023903c4518921d53e3e883"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, - "wx": {:hex, :bridge, "1.0.10", "5051dfe881e498a0bc056603df97b734bfe5fdc084f5e56dc42fab8049247144", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b30941e57a194e557a123c5246f123f8b13acdd5c580f75590a52e72bda15632"}, + "thoas": {:hex, :thoas, "0.4.0", "86a72ccdc5ec388a13f9f843bcd6c1076640233b95440e47ffb8e3c0dbdb5a17", [:rebar3], [], "hexpm", "442296847aca11db8d25180693d7ca3073d6d7179f66952f07b16415306513b6"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/src/azure/b2c.gleam b/src/azure/b2c.gleam new file mode 100644 index 0000000..77cd927 --- /dev/null +++ b/src/azure/b2c.gleam @@ -0,0 +1,321 @@ +//// Implementation of Azure B2C authentication as used by the Thermia API. +//// Mostly translated from: +//// https://github.com/klejejs/python-thermia-online-api/blob/2f0ec4e45bfecbd90932a10247283cbcd6a6c48c/ThermiaOnlineAPI/api/ThermiaAPI.py +//// Used under the Gnu General Public License 3.0 + +import gleam/json +import gleam/base +import gleam/hackney +import gleam/uri +import gleam/http +import gleam/http/request +import gleam/http/response +import gleam/dynamic +import gleam/string +import gleam/list +import gleam/result +import gleam/int +import gleam/io +import azure/utils +import helpers/crypto +import helpers/uri as uri_helpers + +const challenge_length = 43 + +const b2c_client_id = "09ea4903-9e95-45fe-ae1f-e3b7d32fa385" + +const b2c_scope = b2c_client_id + +const b2c_redirect_uri = "https://online-genesis.thermia.se/login" + +const b2c_auth_url = "https://thermialogin.b2clogin.com/thermialogin.onmicrosoft.com/b2c_1a_signuporsigninonline" + +const b2c_authorize_prefix = "var SETTINGS = " + +pub type Tokens { + Tokens(access_token: String, refresh_token: String) +} + +pub type B2CError { + B2CError(msg: String) +} + +type Cookies = + List(#(String, String)) + +type AuthInfo { + AuthInfo(state_code: String, csrf_token: String, cookies: Cookies) +} + +type SelfAsserted { + SelfAsserted(cookies: Cookies) +} + +type Confirmed { + Confirmed(code: String) +} + +/// Authenticate to API with B2C authentication, returning the tokens needed +/// for using the API. +pub fn authenticate( + username: String, + password: String, +) -> Result(Tokens, B2CError) { + let code_challenge = utils.generate_challenge(challenge_length) + try auth_info = authorize(code_challenge) + try self_asserted = signin(username, password, auth_info) + try confirmed = confirm(self_asserted, auth_info) + get_tokens(confirmed, code_challenge) +} + +pub fn refresh(tokens: Tokens) -> Result(Tokens, B2CError) { + todo +} + +fn authorize(code_challenge: String) -> Result(AuthInfo, B2CError) { + let auth_data = [ + #("response_type", "code"), + #("code_challenge", hash_challenge(code_challenge)), + #("code_challenge_method", "S256"), + ..base_request_data() + ] + + let req = build_req(authorize_url(), http.Get) + let req = request.set_query(req, auth_data) + + try resp = run_req(req) + + let body_split = string.split(resp.body, "\n") + try settings = + list.find( + body_split, + fn(line) { string.starts_with(line, b2c_authorize_prefix) }, + ) + |> map_error("Authorize settings string not found.") + + let prefix_len = string.length(b2c_authorize_prefix) + let settings_json = + string.slice(settings, prefix_len, string.length(settings) - prefix_len - 2) + try data = + json.decode(settings_json, using: dynamic.dynamic) + |> map_error( + "Authorize settings JSON parsing error: " <> string.inspect(settings_json), + ) + + try csrf_token = data_get(data, "csrf", dynamic.string) + + try state_code_block = data_get(data, "transId", dynamic.string) + try state_code = + state_code_block + |> string.split("=") + |> list.at(1) + |> map_error("State code parsing error: " <> state_code_block) + + Ok(AuthInfo( + state_code: state_code, + csrf_token: csrf_token, + cookies: response.get_cookies(resp), + )) +} + +fn signin( + username: String, + password: String, + auth_info: AuthInfo, +) -> Result(SelfAsserted, B2CError) { + let self_asserted_data = [ + #("request_type", "RESPONSE"), + #("signInName", username), + #("password", password), + ] + + let req = + build_req(self_asserted_url(), http.Post) + |> request.set_body(uri_helpers.form_urlencoded_serialize( + self_asserted_data, + )) + |> request.set_query(base_query(auth_info)) + |> request.set_header("x-csrf-token", auth_info.csrf_token) + + let req = + list.fold( + auth_info.cookies, + req, + fn(acc, cookie) { + let #(name, value) = cookie + request.set_cookie(acc, name, value) + }, + ) + + try resp = run_req(req) + + case string.contains(resp.body, "{\"status\":\"400\"") { + True -> Error(B2CError(msg: "Wrong credentials.")) + False -> Ok(SelfAsserted(cookies: response.get_cookies(resp))) + } +} + +fn confirm( + self_asserted: SelfAsserted, + auth_info: AuthInfo, +) -> Result(Confirmed, B2CError) { + let csrf_cookie_key = "x-ms-cpim-csrf" + + try csrf_cookie = + list.key_find(auth_info.cookies, csrf_cookie_key) + |> map_error("CSRF cookie not found in auth info.") + let cookies = [#(csrf_cookie_key, csrf_cookie), ..self_asserted.cookies] + + let req = build_req(confirm_url(), http.Get) + + let req = + list.fold( + cookies, + req, + fn(acc, cookie) { request.set_cookie(acc, cookie.0, cookie.1) }, + ) + |> request.set_query([ + #("csrf_token", auth_info.csrf_token), + ..base_query(auth_info) + ]) + + try resp = + req + |> hackney.send() + |> map_error("Confirm HTTP request failed.") + + try resp = case resp.status { + 302 -> Ok(resp) + _ -> Error(B2CError(msg: "Confirm HTTP request bad error code.")) + } + + try location = + response.get_header(resp, "location") + |> map_error("Location not found for confirm response.") + + try code = + location + |> string.split("code=") + |> list.at(1) + |> map_error("Confirmation code not found.") + + Ok(Confirmed(code: code)) +} + +fn get_tokens( + confirmed: Confirmed, + code_challenge: String, +) -> Result(Tokens, B2CError) { + let request_token_data = [ + #("code", confirmed.code), + #("code_verifier", code_challenge), + #("grant_type", "authorization_code"), + ..base_request_data() + ] + + let req = + build_req(get_token_url(), http.Post) + |> request.set_body(uri_helpers.form_urlencoded_serialize( + request_token_data, + )) + + try resp = run_req(req) + try data = + json.decode(resp.body, using: dynamic.dynamic) + |> map_error("Get tokens JSON parsing error: " <> string.inspect(resp.body)) + + try token = data_get(data, "access_token", dynamic.string) + try refresh_token = data_get(data, "refresh_token", dynamic.string) + + Ok(Tokens(access_token: token, refresh_token: refresh_token)) +} + +fn hash_challenge(challenge: String) -> String { + let hashed = crypto.hash(crypto.Sha256, challenge) + base.url_encode64(hashed, False) +} + +fn authorize_url() -> String { + b2c_auth_url <> "/oauth2/v2.0/authorize" +} + +fn self_asserted_url() -> String { + b2c_auth_url <> "/SelfAsserted" +} + +fn confirm_url() -> String { + b2c_auth_url <> "/api/CombinedSigninAndSignup/confirmed" +} + +fn get_token_url() -> String { + b2c_auth_url <> "/oauth2/v2.0/token" +} + +fn build_req(url: String, method: http.Method) -> request.Request(String) { + assert Ok(req_url) = uri.parse(url) + assert Ok(req) = request.from_uri(req_url) + + let req = request.set_method(req, method) + + case method { + http.Post -> + request.set_header( + req, + "content-type", + "application/x-www-form-urlencoded; charset=UTF-8", + ) + _ -> req + } +} + +fn run_req( + req: request.Request(String), +) -> Result(response.Response(String), B2CError) { + try resp = + req + |> hackney.send() + |> map_error("HTTP request failed.") + + case resp.status { + 200 -> Ok(resp) + code -> + Error(B2CError( + msg: "Not OK response code " <> int.to_string(code) <> " from URL " <> uri.to_string(request.to_uri( + req, + )) <> "\n\n" <> string.inspect(resp), + )) + } +} + +fn base_request_data() -> List(#(String, String)) { + [ + #("client_id", b2c_client_id), + #("scope", b2c_scope), + #("redirect_uri", b2c_redirect_uri), + ] +} + +fn base_query(auth_info: AuthInfo) -> List(#(String, String)) { + [ + #("tx", "StateProperties=" <> auth_info.state_code), + #("p", "B2C_1A_SignUpOrSigninOnline"), + ] +} + +fn data_get( + data: dynamic.Dynamic, + key: String, + data_type: dynamic.Decoder(a), +) -> Result(a, B2CError) { + data + |> dynamic.field(key, data_type) + |> map_error( + "Field " <> key <> " of correct type not found in data: " <> string.inspect( + data, + ), + ) +} + +fn map_error(r: Result(a, b), error_msg: String) -> Result(a, B2CError) { + result.map_error(r, fn(_) { B2CError(msg: error_msg) }) +} diff --git a/src/azure/utils.gleam b/src/azure/utils.gleam new file mode 100644 index 0000000..8951862 --- /dev/null +++ b/src/azure/utils.gleam @@ -0,0 +1,28 @@ +import gleam/string +import gleam/list +import gleam/int +import gleam/bit_string +import helpers/binary + +const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +const alphabet_len = 62 + +pub fn generate_challenge(length: Int) -> String { + let alphabet_str = bit_string.from_string(alphabet) + + list.range(0, length - 1) + |> list.map(fn(_) { random_char(alphabet_str) }) + |> string.join(with: "") +} + +fn random_char(alphabet: BitString) -> String { + let index = int.random(0, alphabet_len) + + assert Ok(char) = + alphabet + |> binary.part(index, 1) + |> bit_string.to_string() + + char +} diff --git a/src/helpers/application.gleam b/src/helpers/application.gleam new file mode 100644 index 0000000..5b948b2 --- /dev/null +++ b/src/helpers/application.gleam @@ -0,0 +1,8 @@ +import gleam/erlang/atom.{Atom} +import gleam/dynamic.{Dynamic} + +pub external fn get_env(Atom, Atom) -> Dynamic = + "Elixir.Application" "get_env" + +pub external fn fetch_env_angry(Atom, Atom) -> Dynamic = + "Elixir.Application" "fetch_env!" diff --git a/src/helpers/binary.gleam b/src/helpers/binary.gleam new file mode 100644 index 0000000..3d449e6 --- /dev/null +++ b/src/helpers/binary.gleam @@ -0,0 +1,2 @@ +pub external fn part(data: BitString, pos: Int, len: Int) -> BitString = + "binary" "part" diff --git a/src/helpers/config.gleam b/src/helpers/config.gleam new file mode 100644 index 0000000..c3caae7 --- /dev/null +++ b/src/helpers/config.gleam @@ -0,0 +1,37 @@ +import gleam/erlang/atom +import gleam/dynamic +import gleam/uri +import helpers/application + +fn app_name() -> atom.Atom { + assert Ok(name) = atom.from_string("geo_therminator") + name +} + +pub fn api_timeout() -> Int { + let timeout = + application.fetch_env_angry( + app_name(), + atom.create_from_string("api_timeout"), + ) + + assert Ok(timeout_int) = dynamic.int(timeout) + timeout_int +} + +pub fn api_auth_url() -> uri.Uri { + config_url("api_auth_url") +} + +pub fn api_installations_url() -> uri.Uri { + config_url("api_installations_url") +} + +fn config_url(config_key: String) -> uri.Uri { + let url = + application.fetch_env_angry(app_name(), atom.create_from_string(config_key)) + + assert Ok(url_str) = dynamic.string(url) + assert Ok(parsed_url) = uri.parse(url_str) + parsed_url +} diff --git a/src/helpers/crypto.gleam b/src/helpers/crypto.gleam new file mode 100644 index 0000000..daf366a --- /dev/null +++ b/src/helpers/crypto.gleam @@ -0,0 +1,7 @@ +pub type HashAlgorithm { + // For now, only SHA256 + Sha256 +} + +pub external fn hash(algo: HashAlgorithm, data: String) -> BitString = + "crypto" "hash" diff --git a/src/helpers/date_time.gleam b/src/helpers/date_time.gleam new file mode 100644 index 0000000..5b9555d --- /dev/null +++ b/src/helpers/date_time.gleam @@ -0,0 +1,9 @@ +import gleam/dynamic.{Dynamic} +import gleam/map.{Map} +import gleam/erlang/atom.{Atom} + +pub type DateTime = + Map(Atom, Dynamic) + +pub external fn from_iso8601(String) -> Result(#(DateTime, Int), Atom) = + "Elixir.DateTime" "from_iso8601" diff --git a/src/helpers/io_lib.gleam b/src/helpers/io_lib.gleam new file mode 100644 index 0000000..5c4487c --- /dev/null +++ b/src/helpers/io_lib.gleam @@ -0,0 +1,4 @@ +import gleam/erlang/charlist.{Charlist} + +pub external fn format_int(format: String, data: List(Int)) -> Charlist = + "io_lib" "format" diff --git a/src/helpers/uri.gleam b/src/helpers/uri.gleam new file mode 100644 index 0000000..44a1489 --- /dev/null +++ b/src/helpers/uri.gleam @@ -0,0 +1,47 @@ +import gleam/string +import gleam/string_builder +import gleam/list +import gleam/uri + +const not_encoded_by_gleam = [ + #("!", "%21"), + #("$", "%24"), + #("'", "%27"), + #("(", "%28"), + #(")", "%29"), + #("+", "%2B"), + #("~", "%7E"), +] + +/// x-www-form-urlencoded encoder, follows the spec defined in +/// https://url.spec.whatwg.org/#application/x-www-form-urlencoded +pub fn form_urlencoded_serialize(data: List(#(String, String))) -> String { + data + |> list.map(fn(item) { + let #(key, value) = item + serialize_value(key) + |> string_builder.append("=") + |> string_builder.append_builder(serialize_value(value)) + }) + |> string_builder.join("&") + |> string_builder.to_string() +} + +fn serialize_value(value: String) -> string_builder.StringBuilder { + value + |> string.split(" ") + |> list.map(fn(part) { + let encoded = uri.percent_encode(part) + + list.fold( + not_encoded_by_gleam, + encoded, + fn(acc, item) { + let #(char, replacement) = item + string.replace(acc, char, replacement) + }, + ) + |> string_builder.from_string() + }) + |> string_builder.join("+") +} diff --git a/src/pump_api/auth/api.gleam b/src/pump_api/auth/api.gleam new file mode 100644 index 0000000..d6deefd --- /dev/null +++ b/src/pump_api/auth/api.gleam @@ -0,0 +1,104 @@ +import gleam/http/request +import gleam/json +import gleam/hackney +import gleam/result +import gleam/dynamic +import gleam/list +import pump_api/auth/user.{User} +import pump_api/auth/token.{Token} +import pump_api/auth/installation_info.{InstallationInfo} +import pump_api/http +import helpers/config +import helpers/date_time + +pub type ApiError { + ApiRequestFailed + NotOkResponse + InvalidData +} + +pub fn auth(username: String, password: String) -> Result(User, ApiError) { + let url = config.api_auth_url() + assert Ok(raw_req) = request.from_uri(url) + + let data = + json.object([ + #("username", json.string(username)), + #("password", json.string(password)), + ]) + + let json_req = request.set_body(raw_req, http.Json(data)) + try data = run_req(http.req(json_req)) + + try token_str = data_get(data, "token", dynamic.string) + try valid_to = data_get(data, "tokenValidToUtc", dynamic.string) + try valid_to_dt = + date_time.from_iso8601(valid_to) + |> map_error(InvalidData) + + let token = Token(token: token_str, token_valid_to: valid_to_dt.0) + + try user_name = data_get(data, "userName", dynamic.string) + try email = data_get(data, "email", dynamic.string) + try first_name = data_get(data, "firstName", dynamic.string) + try last_name = data_get(data, "lastName", dynamic.string) + try culture = data_get(data, "culture", dynamic.string) + try eula_accepted = data_get(data, "eulaAccepted", dynamic.bool) + try is_authenticated = data_get(data, "isAuthenticated", dynamic.bool) + try time_zone = data_get(data, "timeZone", dynamic.string) + + Ok(User( + user_name: user_name, + email: email, + first_name: first_name, + last_name: last_name, + culture: culture, + eula_accepted: eula_accepted, + is_authenticated: is_authenticated, + time_zone: time_zone, + token: token, + )) +} + +pub fn installation_info(user: User) -> Result(List(InstallationInfo), ApiError) { + let url = config.api_installations_url() + assert Ok(raw_req) = request.from_uri(url) + + let empty_req = request.set_body(raw_req, http.Empty) + try data = run_req(http.authed_req(user, empty_req)) + + try items = + data_get(data, "items", dynamic.list(of: dynamic.field("id", dynamic.int))) + + Ok(list.map(items, fn(id) { InstallationInfo(id: id) })) +} + +fn run_req(req: request.Request(String)) { + try resp = + req + |> hackney.send() + |> map_error(ApiRequestFailed) + + try body = case resp.status { + 200 -> Ok(resp.body) + _ -> Error(NotOkResponse) + } + + body + |> json.decode(using: dynamic.dynamic) + |> map_error(InvalidData) +} + +fn data_get( + data: dynamic.Dynamic, + key: String, + data_type: dynamic.Decoder(a), +) -> Result(a, ApiError) { + data + |> dynamic.field(key, data_type) + |> map_error(InvalidData) +} + +fn map_error(r: Result(a, b), new_error: ApiError) -> Result(a, ApiError) { + result.map_error(r, fn(_) { new_error }) +} diff --git a/src/pump_api/auth/installation_info.gleam b/src/pump_api/auth/installation_info.gleam new file mode 100644 index 0000000..d37ef73 --- /dev/null +++ b/src/pump_api/auth/installation_info.gleam @@ -0,0 +1,3 @@ +pub type InstallationInfo { + InstallationInfo(id: Int) +} diff --git a/src/pump_api/auth/token.gleam b/src/pump_api/auth/token.gleam new file mode 100644 index 0000000..82b1fca --- /dev/null +++ b/src/pump_api/auth/token.gleam @@ -0,0 +1,5 @@ +import helpers/date_time.{DateTime} + +pub type Token { + Token(token: String, token_valid_to: DateTime) +} diff --git a/src/pump_api/auth/user.gleam b/src/pump_api/auth/user.gleam new file mode 100644 index 0000000..6c97ac5 --- /dev/null +++ b/src/pump_api/auth/user.gleam @@ -0,0 +1,15 @@ +import pump_api/auth/token + +pub type User { + User( + user_name: String, + email: String, + first_name: String, + last_name: String, + culture: String, + eula_accepted: Bool, + is_authenticated: Bool, + time_zone: String, + token: token.Token, + ) +} diff --git a/src/pump_api/http.gleam b/src/pump_api/http.gleam new file mode 100644 index 0000000..6f3dc6c --- /dev/null +++ b/src/pump_api/http.gleam @@ -0,0 +1,31 @@ +import gleam/http/request +import gleam/json.{Json} +import pump_api/auth/user.{User} + +pub type Body { + Empty + Json(data: Json) + String(data: String) +} + +pub type ApiRequest = + request.Request(Body) + +pub fn authed_req(user: User, r: ApiRequest) { + r + |> request.set_header("authorization", "Bearer " <> user.token.token) + |> req() +} + +pub fn req(r: ApiRequest) -> request.Request(String) { + let r = request.set_header(r, "accept", "application/json") + + case r.body { + Empty -> request.set_body(r, "") + String(data) -> request.set_body(r, data) + Json(data) -> + r + |> request.set_header("content-type", "application/json") + |> request.set_body(json.to_string(data)) + } +}