infrastructure

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit fc8038ad0065740073174f9f1facc6928522f0f1
parent 031131697a9c3737367436d0fcee822c043c8391
Author: Silas Brack <silasbrack@gmail.com>
Date:   Thu, 10 Oct 2024 20:03:39 +0200

Move to flake

Diffstat:
A.sops.yaml | 13+++++++++++++
Dapps/my-app/README.md | 16----------------
Dapps/my-app/client/.github/workflows/test.yml | 23-----------------------
Dapps/my-app/client/gleam.toml | 23-----------------------
Dapps/my-app/client/manifest.toml | 55-------------------------------------------------------
Dapps/my-app/client/shell.nix | 15---------------
Dapps/my-app/client/src/client.gleam | 860-------------------------------------------------------------------------------
Dapps/my-app/client/src/client/state.gleam | 51---------------------------------------------------
Dapps/my-app/server/.github/workflows/test.yml | 23-----------------------
Dapps/my-app/server/.gitignore | 8--------
Dapps/my-app/server/gleam.toml | 39---------------------------------------
Dapps/my-app/server/manifest.toml | 60------------------------------------------------------------
Dapps/my-app/server/src/erlib.erl | 7-------
Dapps/my-app/server/src/server.gleam | 38--------------------------------------
Dapps/my-app/server/src/server/actors.gleam | 12------------
Dapps/my-app/server/src/server/actors/calculator_actor.gleam | 72------------------------------------------------------------------------
Dapps/my-app/server/src/server/actors/room_actor.gleam | 80-------------------------------------------------------------------------------
Dapps/my-app/server/src/server/actors/websocket_actor.gleam | 191-------------------------------------------------------------------------------
Dapps/my-app/server/src/server/db.gleam | 24------------------------
Dapps/my-app/server/src/server/db/datetime_helper.gleam | 31-------------------------------
Dapps/my-app/server/src/server/db/login.gleam | 68--------------------------------------------------------------------
Dapps/my-app/server/src/server/db/post.gleam | 30------------------------------
Dapps/my-app/server/src/server/env.gleam | 46----------------------------------------------
Dapps/my-app/server/src/server/process_utils.gleam | 4----
Dapps/my-app/server/src/server/response.gleam | 23-----------------------
Dapps/my-app/server/src/server/router.gleam | 339-------------------------------------------------------------------------------
Dapps/my-app/server/src/server/routes/posts.gleam | 67-------------------------------------------------------------------
Dapps/my-app/server/src/server/scaffold.gleam | 61-------------------------------------------------------------
Dapps/my-app/server/src/server/web.gleam | 52----------------------------------------------------
Dapps/my-app/server/test/server_test.gleam | 49-------------------------------------------------
Dapps/my-app/shared/.github/workflows/test.yml | 23-----------------------
Dapps/my-app/shared/.gitignore | 4----
Dapps/my-app/shared/gleam.toml | 22----------------------
Dapps/my-app/shared/manifest.toml | 16----------------
Dapps/my-app/shared/shell.nix | 15---------------
Dapps/my-app/shared/src/shared.gleam | 61-------------------------------------------------------------
Dapps/my-app/shared/test/shared_test.gleam | 12------------
Aapps/my_app/.github/workflows/test.yml | 23+++++++++++++++++++++++
Rapps/my-app/client/.gitignore -> apps/my_app/.gitignore | 0
Aapps/my_app/README.md | 24++++++++++++++++++++++++
Aapps/my_app/build.sh | 6++++++
Aapps/my_app/flake.lock | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapps/my_app/flake.nix | 29+++++++++++++++++++++++++++++
Aapps/my_app/gleam.toml | 24++++++++++++++++++++++++
Aapps/my_app/manifest.toml | 37+++++++++++++++++++++++++++++++++++++
Rapps/my-app/default.nix -> apps/my_app/module.nix | 0
Rapps/my-app/server/shell.nix -> apps/my_app/shell.nix | 0
Aapps/my_app/src/my_app.gleam | 26++++++++++++++++++++++++++
Aapps/my_app/src/my_app/router.gleam | 16++++++++++++++++
Aapps/my_app/src/my_app/web.gleam | 32++++++++++++++++++++++++++++++++
Rapps/my-app/client/test/client_test.gleam -> apps/my_app/test/my_app_test.gleam | 0
Aflake.lock | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aflake.nix | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minfrastructure/hive.nix | 10+++++-----
Asecrets.yaml | 40++++++++++++++++++++++++++++++++++++++++
Ashell.nix | 11+++++++++++
Avpn.nix | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
57 files changed, 623 insertions(+), 2525 deletions(-)

diff --git a/.sops.yaml b/.sops.yaml @@ -0,0 +1,13 @@ +keys: + - &users: + - &desktop age1s29qgfupgx7mqn9dk2rndp6xh63f5qan8vqa77ve603wd5m9a9rqkpq7re + - &thinkpad age10h669gfc0yfdrfj4gtnsrpdfvrsh55tn0cj8kuk5t3x5e079dpgs30cuxw + - &hosts: + - &server age1e52v0cr69sycuh59hksx9nk5327vgkemkxqwngd25gtztxhkpups87yd5g +creation_rules: + - path_regex: secrets.yaml + key_groups: + - age: + - *desktop + - *thinkpad + - *server diff --git a/apps/my-app/README.md b/apps/my-app/README.md @@ -1,16 +0,0 @@ -# My App - -## Migrations - -Set environment variable `PGPASSWORD` -``` -LATEST_VERSION = $(psql -U ${PGUSER} -h ${PGHOST} -p ${PGPORT} -d ${PGDATABASE} --set=sslmode=verify-full -AXqtc 'SELECT MAX(version) FROM migrations;') -LATEST_VERSION = ${LATEST_VERSION:-0} -MAX_VERSION = $( ls ./migrations/ | sort -n | tail -1 | xargs -I _ basename _ .sql | xargs -I _ echo $(( _ )) ) -for VERSION in $(seq -f "%04g" $LATEST_VERSION $MAX_VERSION) -do - psql -U ${PGUSER} -h ${PGHOST} -p ${PGPORT} -d ${PGDATABASE} --set=sslmode=verify-full -a -f migrations/$VERSION.sql - psql -U ${PGUSER} -h ${PGHOST} -p ${PGPORT} -d ${PGDATABASE} --set=sslmode=verify-full -a -c 'INSERT INTO migrations (version) VALUES ('$(( $VERSION ))');' -done -``` - diff --git a/apps/my-app/client/.github/workflows/test.yml b/apps/my-app/client/.github/workflows/test.yml @@ -1,23 +0,0 @@ -name: test - -on: - push: - branches: - - master - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - with: - otp-version: "26.0.2" - gleam-version: "1.1.0" - rebar3-version: "3" - # elixir-version: "1.15.4" - - run: gleam deps download - - run: gleam test - - run: gleam format --check src test diff --git a/apps/my-app/client/gleam.toml b/apps/my-app/client/gleam.toml @@ -1,23 +0,0 @@ -name = "client" -version = "1.0.0" -target = "javascript" - -# Fill out these fields if you intend to generate HTML documentation or publish -# your project to the Hex package manager. -# -# description = "" -# licences = ["Apache-2.0"] -# repository = { type = "github", user = "username", repo = "project" } -# links = [{ title = "Website", href = "https://gleam.run" }] -# -# For a full reference of all the available options, you can have a look at -# https://gleam.run/writing-gleam/gleam-toml/. - -[dependencies] -gleam_stdlib = ">= 0.34.0 and < 2.0.0" -lustre_http = ">= 0.5.2 and < 1.0.0" -shared = { path = "../shared/" } - -[dev-dependencies] -gleeunit = ">= 1.0.0 and < 2.0.0" -lustre_dev_tools = ">= 1.5.8 and < 2.0.0" diff --git a/apps/my-app/client/manifest.toml b/apps/my-app/client/manifest.toml @@ -1,55 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, - { name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, - { name = "gleam_community_ansi", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "4CD513FC62523053E62ED7BAC2F36136EC17D6A8942728250A9A00A15E340E4B" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_httpc", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF6CDD88830CC9853F7638ECC0BE7D7CD9522640DA5FAB4C08CFAC8DEBD08028" }, - { name = "gleam_javascript", version = "0.12.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "6EB652538B31E852FE0A8307A8B6314DEB34930944B6DDC41CCC31CA344DA35D" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.11.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "517FFB679E44AD71D059F3EF6A17BA6EFC8CB94FA174D52E22FB6768CF684D78" }, - { name = "gleam_package_interface", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "CF3BFC5D0997750D9550D8D73A90F4B8D71C6C081B20ED4E70FFBE1E99AFC3C2" }, - { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, - { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" }, - { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, - { name = "glint", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "369C8A289017F73581D6B9FE2F5748169EB6FA021FFAA34FA7A49EE2094C73B3" }, - { name = "glisten", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "48EF7F6D1DCA877C2F49AF35CC33946C7129EEB05A114758A2CC569C708BFAF8" }, - { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, - { name = "lustre", version = "4.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "B64EFA26B6DDA8391C85E33B06C2B5F4CD24B9772342824085DBC445B7E05C5A" }, - { name = "lustre_dev_tools", version = "1.5.8", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "C65F0A3AB666988B201101E5804CDB9EDFFF7773A3AF9A2CE33392C4FFB86612" }, - { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" }, - { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, - { name = "mist", version = "2.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "981F12FC8BA0656B40099EC876D6F2BEE7B95593610F342E9AB0DC4E663A932F" }, - { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, - { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], source = "local", path = "../shared" }, - { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, - { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, - { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "228E667239504B57AD05EC3C332C930391592F6C974D0EFECF32FFD0F3629A27" }, - { name = "wisp", version = "1.1.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "5B25E37E08A94039668D2ACF7B2D2A665BEAF4B48EFA8613010B4E3164137664" }, -] - -[requirements] -gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } -gleeunit = { version = ">= 1.0.0 and < 2.0.0" } -lustre_dev_tools = { version = ">= 1.5.8 and < 2.0.0"} -lustre_http = { version = ">= 0.5.2 and < 1.0.0" } -shared = { path = "../shared/" } diff --git a/apps/my-app/client/shell.nix b/apps/my-app/client/shell.nix @@ -1,15 +0,0 @@ -let - pkgs = import <nixpkgs> {}; - # pkgs = import (builtins.fetchTarball { - # name = "nixos-24.05"; - # url = "https://github.com/nixos/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.gz"; - # sha256 = "1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; - # }) {}; -in pkgs.mkShell { - buildInputs = [ - pkgs.gleam - pkgs.erlang_27 - pkgs.rebar3 - ]; - # imports = ["./test.nix"]; -} diff --git a/apps/my-app/client/src/client.gleam b/apps/my-app/client/src/client.gleam @@ -1,860 +0,0 @@ -import client/components/button.{button_class} -import client/components/like.{like_comment, like_post} -import client/routes/change_password.{change_password} -import client/routes/create_post.{create_post_view} -import client/routes/forgot_password.{forgot_password} -import client/routes/latest.{latest_view} -import client/routes/login.{login, login_view} -import client/routes/post.{show_post_view} -import client/routes/signup.{signup_view} -import client/routes/user.{user_view} -import client/state.{ - type Model, type Msg, type Route, Active, AuthUser, AuthUserRecieved, - ChangePassword, ChangePasswordResponded, ChangePasswordTargetRecieved, - CreateAuthCodeResponded, CreateCommentResponded, CreateCommentUpdateBody, - CreateCommentUpdateError, CreateCommentUpdateParentId, CreatePost, - CreatePostResponded, CreatePostUpdateBody, CreatePostUpdateError, - CreatePostUpdateHref, CreatePostUpdateOriginalCreator, CreatePostUpdateTags, - CreatePostUpdateTitle, CreatePostUpdateUseBody, ForgotPassword, - ForgotPasswordResponded, GetPostsResponse, GetTagsResponse, InviterRecieved, - LikeCommentResponded, LikePostResponded, Login, LoginResponded, - LoginUpdateEmailUsername, LoginUpdateError, LoginUpdatePassword, - LogoutResponded, Model, NotFound, OnRouteChange, PostsRecieved, - RequestChangePassword, RequestCreateAuthCode, RequestCreateComment, - RequestCreatePost, RequestForgotPassword, RequestLikeComment, RequestLikePost, - RequestLogin, RequestLogout, RequestSignUp, ShowPost, ShowPostRecieved, - SignUpResponded, SignUpUpdateEmail, SignUpUpdateError, SignUpUpdatePassword, - SignUpUpdateUsername, Signup, TagsRecieved, UserPage, UsernameResponse, - message_error_decoder, -} -import decode -import env -import gleam/dynamic -import gleam/int -import gleam/json -import gleam/list -import gleam/option.{None, Some} -import gleam/uri.{type Uri} -import lustre -import lustre/attribute.{class, href, id, src} -import lustre/effect.{type Effect} -import lustre/element.{type Element, text} -import lustre/element/html.{a, body, footer, img, nav, p, span} -import lustre_http -import modem -import shared.{type Post, type PostComment, type Tag, Post, PostComment, Tag} - -pub fn main() { - lustre.application(init, update, view) - |> lustre.start("#app", Nil) -} - -fn init(_) -> #(Model, Effect(Msg)) { - #( - Model( - route: get_route(), - inviter: "", - auth_user: None, - sign_up_username: "", - sign_up_email: "", - sign_up_password: "", - sign_up_error: None, - login_email_username: "", - login_password: "", - login_error: None, - create_post_title: "", - create_post_href: "", - create_post_body: "", - create_post_original_creator: False, - create_post_tags: [], - create_post_use_body: False, - create_post_error: None, - posts: [], - show_post: None, - create_comment_body: "", - create_comment_parent_id: None, - create_comment_error: None, - tags: [], - invite_link: None, - forgot_password_response: None, - change_password_target: "", - ), - effect.batch( - [modem.init(on_url_change), get_auth_user(), get_posts()] - |> list.append(case get_route() { - ShowPost(_) -> [get_show_post()] - Signup(_) -> [get_inviter(get_auth_code())] - ChangePassword(token) -> [get_change_password_target(token)] - _ -> [] - }), - ), - ) -} - -fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { - case msg { - OnRouteChange(route) -> #( - Model( - ..model, - route: route, - show_post: case route { - ShowPost(_) -> None - _ -> model.show_post - }, - ), - case route { - ShowPost(_) -> get_show_post() - CreatePost -> - case model.tags { - [] -> get_tags() - _ -> effect.none() - } - _ -> effect.none() - }, - ) - InviterRecieved(auth_code_result) -> - case auth_code_result { - Ok(res) -> #(Model(..model, inviter: res.username), effect.none()) - Error(_) -> #(model, effect.none()) - } - AuthUserRecieved(auth_user_result) -> - case auth_user_result { - Ok(auth_user) -> #(Model(..model, auth_user: Some(auth_user)), case - get_route() - { - CreatePost -> - case uri.parse("/create-post") { - Ok(uri) -> - effect.from(fn(dispatch) { - on_url_change(uri) - |> dispatch - }) - Error(_) -> effect.none() - } - _ -> effect.none() - }) - Error(_) -> #(model, effect.none()) - } - PostsRecieved(get_posts_result) -> - case get_posts_result { - Ok(get_posts) -> #( - Model(..model, posts: get_posts.posts), - effect.none(), - ) - Error(_) -> #(model, effect.none()) - } - ShowPostRecieved(get_post_result) -> - case get_post_result { - Ok(get_post) -> #( - Model(..model, show_post: Some(get_post)), - effect.none(), - ) - Error(_) -> #(model, effect.none()) - } - TagsRecieved(get_tags_result) -> - case get_tags_result { - Ok(get_tags) -> #(Model(..model, tags: get_tags.tags), effect.none()) - Error(_) -> #(model, effect.none()) - } - - SignUpUpdateUsername(value) -> #( - Model(..model, sign_up_username: value), - effect.none(), - ) - SignUpUpdateEmail(value) -> #( - Model(..model, sign_up_email: value), - effect.none(), - ) - SignUpUpdatePassword(value) -> #( - Model(..model, sign_up_password: value), - effect.none(), - ) - SignUpUpdateError(value) -> #( - Model(..model, sign_up_error: value), - effect.none(), - ) - RequestSignUp -> #(model, signup(model)) - SignUpResponded(resp_result) -> - case resp_result { - Ok(resp) -> - case resp.error { - Some(err) -> #( - model, - effect.from(fn(dispatch) { - dispatch(SignUpUpdateError(Some(err))) - }), - ) - None -> #( - Model( - ..model, - sign_up_username: "", - sign_up_email: "", - sign_up_password: "", - sign_up_error: None, - ), - effect.batch([ - modem.push("/", None, None), - get_auth_user(), - get_show_post(), - get_posts(), - ]), - ) - } - Error(_) -> #( - model, - effect.from(fn(dispatch) { - dispatch(SignUpUpdateError(Some("HTTP Error"))) - }), - ) - } - - LoginUpdateEmailUsername(value) -> #( - Model(..model, login_email_username: value), - effect.none(), - ) - LoginUpdatePassword(value) -> #( - Model(..model, login_password: value), - effect.none(), - ) - LoginUpdateError(value) -> #( - Model(..model, login_error: value), - effect.none(), - ) - RequestLogin -> #(model, login(model)) - LoginResponded(resp_result) -> - case resp_result { - Ok(resp) -> - case resp.error { - Some(err) -> #( - model, - effect.from(fn(dispatch) { dispatch(LoginUpdateError(Some(err))) }), - ) - None -> #( - Model( - ..model, - login_email_username: "", - login_password: "", - login_error: None, - ), - effect.batch([ - modem.push("/", None, None), - get_auth_user(), - get_posts(), - get_show_post(), - ]), - ) - } - Error(_) -> #( - model, - effect.from(fn(dispatch) { - dispatch(LoginUpdateError(Some("HTTP Error"))) - }), - ) - } - - RequestLogout -> #(model, logout(model)) - LogoutResponded(_) -> #( - Model(..model, auth_user: None), - effect.batch([ - modem.push("/", None, None), - get_posts(), - get_show_post(), - get_auth_user(), - ]), - ) - - CreatePostUpdateTitle(value) -> #( - Model(..model, create_post_title: value), - effect.none(), - ) - CreatePostUpdateHref(value) -> #( - Model(..model, create_post_href: value), - effect.none(), - ) - CreatePostUpdateBody(value) -> #( - Model(..model, create_post_body: value), - effect.none(), - ) - CreatePostUpdateOriginalCreator(value) -> #( - Model(..model, create_post_original_creator: value), - effect.none(), - ) - CreatePostUpdateTags(tag_id) -> #( - Model( - ..model, - create_post_tags: case list.contains(model.create_post_tags, tag_id) { - True -> list.filter(model.create_post_tags, fn(tag) { tag != tag_id }) - False -> [tag_id, ..model.create_post_tags] - }, - ), - effect.none(), - ) - CreatePostUpdateUseBody(value) -> #( - Model(..model, create_post_use_body: value, create_post_href: ""), - effect.none(), - ) - CreatePostUpdateError(value) -> #( - Model(..model, create_post_error: value), - effect.none(), - ) - RequestCreatePost -> #(model, create_post(model)) - CreatePostResponded(resp_result) -> - case resp_result { - Ok(resp) -> - case resp.error { - Some(err) -> #( - model, - effect.from(fn(dispatch) { - dispatch(CreatePostUpdateError(Some(err))) - }), - ) - None -> #( - Model( - ..model, - create_post_title: "", - create_post_href: "", - create_post_tags: [], - create_post_error: None, - ), - effect.batch([modem.push("/", None, None), get_posts()]), - ) - } - Error(_) -> #( - model, - effect.from(fn(dispatch) { - dispatch(CreatePostUpdateError(Some("HTTP Error"))) - }), - ) - } - - RequestLikePost(post_id) -> #(model, like_post(post_id)) - LikePostResponded(_) -> #(model, case model.show_post { - Some(_) -> get_show_post() - None -> get_posts() - }) - - CreateCommentUpdateBody(body) -> #( - Model(..model, create_comment_body: body), - effect.none(), - ) - CreateCommentUpdateParentId(value) -> #( - Model(..model, create_comment_parent_id: value), - effect.none(), - ) - CreateCommentUpdateError(value) -> #( - Model(..model, create_comment_error: value), - effect.none(), - ) - RequestCreateComment -> #(model, create_comment(model)) - CreateCommentResponded(resp_result) -> - case resp_result { - Ok(resp) -> - case resp.error { - Some(err) -> #( - model, - effect.from(fn(dispatch) { - dispatch(CreateCommentUpdateError(Some(err))) - }), - ) - None -> #( - Model( - ..model, - create_comment_parent_id: None, - create_comment_body: "", - create_comment_error: None, - ), - get_show_post(), - ) - } - Error(_) -> #( - model, - effect.from(fn(dispatch) { - dispatch(CreatePostUpdateError(Some("HTTP Error"))) - }), - ) - } - - RequestLikeComment(post_comment_id) -> #( - model, - like_comment(post_comment_id), - ) - LikeCommentResponded(_) -> #(model, get_show_post()) - - RequestCreateAuthCode -> #(model, create_auth_code()) - CreateAuthCodeResponded(resp_result) -> - case resp_result { - Ok(resp) -> - case resp.message { - Some(code) -> { - set_clipboard(env.get_api_url() <> "/auth/signup/" <> code) - #( - Model( - ..model, - invite_link: Some( - env.get_api_url() <> "/auth/signup/" <> code, - ), - ), - effect.none(), - ) - } - None -> #(model, effect.none()) - } - Error(_) -> #(model, effect.none()) - } - - RequestForgotPassword -> #(model, request_forgot_password(model)) - ForgotPasswordResponded(resp_result) -> - case resp_result { - Ok(resp) -> #( - Model( - ..model, - login_email_username: "", - forgot_password_response: case resp.message { - Some(message) -> Some(Ok(message)) - None -> - case resp.error { - Some(error) -> Some(Error(error)) - None -> None - } - }, - ), - effect.none(), - ) - Error(_) -> #( - Model(..model, forgot_password_response: Some(Error("HTTP Error"))), - effect.none(), - ) - } - ChangePasswordTargetRecieved(target_response) -> - case target_response { - Ok(res) -> #( - Model(..model, change_password_target: res.username), - effect.none(), - ) - Error(_) -> #(model, effect.none()) - } - - RequestChangePassword -> #(model, send_password_change(model)) - ChangePasswordResponded(resp_result) -> - case resp_result { - Ok(_) -> #( - Model(..model, login_password: ""), - modem.push("/auth/login", None, None), - ) - Error(_) -> #( - Model(..model, forgot_password_response: Some(Error("HTTP Error"))), - effect.none(), - ) - } - } -} - -fn on_url_change(uri: Uri) -> Msg { - set_url(uri.path) - OnRouteChange(get_route()) -} - -@external(javascript, "./ffi.mjs", "set_clipboard") -fn set_clipboard(text: String) -> String - -@external(javascript, "./ffi.mjs", "get_route") -fn do_get_route() -> String - -@external(javascript, "./ffi.mjs", "set_url") -fn set_url(url: String) -> String - -fn get_route() -> Route { - let uri = case do_get_route() |> uri.parse { - Ok(uri) -> uri - _ -> panic as "Invalid uri" - } - - case uri.path |> uri.path_segments { - [] -> Active - ["auth", "login"] -> Login - ["auth", "signup", auth_code] -> Signup(auth_code: auth_code) - ["auth", "forgot-password"] -> ForgotPassword - ["auth", "forgot-password", token] -> ChangePassword(token) - ["create-post"] -> CreatePost - ["user", username] -> UserPage(username) - ["post", post_id] -> - case int.parse(post_id) { - Ok(id) -> ShowPost(id) - Error(_) -> NotFound - } - _ -> NotFound - } -} - -fn request_forgot_password(model: Model) { - lustre_http.post( - env.get_api_url() <> "/api/auth/forgot-password", - json.object([#("email", json.string(model.login_email_username))]), - lustre_http.expect_json(message_error_decoder(), ForgotPasswordResponded), - ) -} - -fn create_auth_code() { - lustre_http.post( - env.get_api_url() <> "/api/auth-code", - json.object([]), - lustre_http.expect_json(message_error_decoder(), CreateAuthCodeResponded), - ) -} - -fn get_auth_code() -> String { - let uri = case do_get_route() |> uri.parse { - Ok(uri) -> uri - _ -> panic as "Invalid uri" - } - - case uri.path |> uri.path_segments { - ["auth", "signup", auth_code] -> auth_code - _ -> "1" - } -} - -fn get_forgot_password_token() -> String { - let uri = case do_get_route() |> uri.parse { - Ok(uri) -> uri - _ -> panic as "Invalid uri" - } - - case uri.path |> uri.path_segments { - ["auth", "forgot-password", token] -> token - _ -> "1" - } -} - -fn get_post_id() -> String { - let uri = case do_get_route() |> uri.parse { - Ok(uri) -> uri - _ -> panic as "Invalid uri" - } - - case uri.path |> uri.path_segments { - ["post", post_id] -> post_id - _ -> "" - } -} - -pub fn get_inviter(auth_code: String) -> Effect(Msg) { - let url = env.get_api_url() <> "/api/auth-code/" <> auth_code - let decoder = - dynamic.decode1(UsernameResponse, dynamic.field("username", dynamic.string)) - - lustre_http.get(url, lustre_http.expect_json(decoder, InviterRecieved)) -} - -pub fn get_change_password_target(token: String) -> Effect(Msg) { - let url = env.get_api_url() <> "/api/auth/forgot-password/" <> token - let decoder = - dynamic.decode1(UsernameResponse, dynamic.field("username", dynamic.string)) - - lustre_http.get( - url, - lustre_http.expect_json(decoder, ChangePasswordTargetRecieved), - ) -} - -pub fn get_auth_user() -> Effect(Msg) { - let url = env.get_api_url() <> "/api/auth/validate" - - let decoder = - dynamic.decode3( - AuthUser, - dynamic.field("user_id", dynamic.int), - dynamic.field("username", dynamic.string), - dynamic.field("is_admin", dynamic.bool), - ) - - lustre_http.get(url, lustre_http.expect_json(decoder, AuthUserRecieved)) -} - -pub fn get_show_post() -> Effect(Msg) { - let url = env.get_api_url() <> "/api/posts/" <> get_post_id() - - lustre_http.get( - url, - lustre_http.expect_json( - fn(data) { decode.from(post_decoder(), data) }, - ShowPostRecieved, - ), - ) -} - -pub fn post_decoder() { - decode.into({ - use id <- decode.parameter - use title <- decode.parameter - use href <- decode.parameter - use body <- decode.parameter - use likes <- decode.parameter - use user_like_post <- decode.parameter - use comments_count <- decode.parameter - use comments <- decode.parameter - use tags <- decode.parameter - use username <- decode.parameter - use original_creator <- decode.parameter - use created_at <- decode.parameter - - Post( - id, - title, - href, - body, - likes, - user_like_post, - comments_count, - comments, - tags, - username, - original_creator, - created_at, - ) - }) - |> decode.field("id", decode.int) - |> decode.field("title", decode.string) - |> decode.field("href", decode.optional(decode.string)) - |> decode.field("body", decode.optional(decode.string)) - |> decode.field("likes", decode.int) - |> decode.field("user_like_post", decode.bool) - |> decode.field("comments_count", decode.int) - |> decode.field("comments", decode.list(comment_decoder())) - |> decode.field("tags", decode.list(decode.string)) - |> decode.field("username", decode.string) - |> decode.field("original_creator", decode.bool) - |> decode.field("created_at", decode.int) -} - -fn comment_decoder() { - decode.into({ - use id <- decode.parameter - use body <- decode.parameter - use username <- decode.parameter - use likes <- decode.parameter - use user_like_post_comment <- decode.parameter - use parent_id <- decode.parameter - use created_at <- decode.parameter - - PostComment( - id, - body, - username, - likes, - user_like_post_comment, - parent_id, - created_at, - ) - }) - |> decode.field("id", decode.int) - |> decode.field("body", decode.string) - |> decode.field("username", decode.string) - |> decode.field("likes", decode.int) - |> decode.field("user_like_post_comment", decode.bool) - |> decode.field("parent_id", decode.optional(decode.int)) - |> decode.field("created_at", decode.int) -} - -pub fn get_posts() -> Effect(Msg) { - let url = env.get_api_url() <> "/api/posts" - - let response_decoder = - decode.into({ - use posts <- decode.parameter - - GetPostsResponse(posts) - }) - |> decode.field("posts", decode.list(post_decoder())) - - lustre_http.get( - url, - lustre_http.expect_json( - fn(data) { response_decoder |> decode.from(data) }, - PostsRecieved, - ), - ) -} - -pub fn tag_decoder() { - decode.into({ - use id <- decode.parameter - use name <- decode.parameter - use category <- decode.parameter - use permission <- decode.parameter - - Tag( - id, - name, - category: shared.string_to_tag_category(category), - permission: shared.string_to_tag_permission(permission), - ) - }) - |> decode.field("id", decode.int) - |> decode.field("name", decode.string) - |> decode.field("category", decode.string) - |> decode.field("permission", decode.string) -} - -pub fn get_tags() -> Effect(Msg) { - let url = env.get_api_url() <> "/api/tags" - - let response_decoder = - decode.into({ - use tags <- decode.parameter - - GetTagsResponse(tags) - }) - |> decode.field("tags", decode.list(tag_decoder())) - - lustre_http.get( - url, - lustre_http.expect_json( - fn(data) { response_decoder |> decode.from(data) }, - TagsRecieved, - ), - ) -} - -fn signup(model: Model) { - lustre_http.post( - env.get_api_url() <> "/api/users", - json.object([ - #("username", json.string(model.sign_up_username)), - #("email", json.string(model.sign_up_email)), - #("password", json.string(model.sign_up_password)), - #("auth_code", json.string(get_auth_code())), - ]), - lustre_http.expect_json(message_error_decoder(), SignUpResponded), - ) -} - -fn logout(model _: Model) { - lustre_http.post( - env.get_api_url() <> "/api/auth/logout", - json.object([]), - lustre_http.expect_json(message_error_decoder(), LogoutResponded), - ) -} - -fn create_post(model: Model) { - lustre_http.post( - env.get_api_url() <> "/api/posts", - json.object([ - #("title", json.string(model.create_post_title)), - #("original_creator", json.bool(model.create_post_original_creator)), - case !model.create_post_use_body { - True -> #("href", json.string(model.create_post_href)) - False -> #("body", json.string(model.create_post_body)) - }, - #("tags", json.array(model.create_post_tags, fn(tag) { json.int(tag) })), - ]), - lustre_http.expect_json(message_error_decoder(), CreatePostResponded), - ) -} - -fn send_password_change(model: Model) { - lustre_http.post( - env.get_api_url() - <> "/api/auth/change-password/" - <> get_forgot_password_token(), - json.object([#("password", json.string(model.login_password))]), - lustre_http.expect_json(message_error_decoder(), ChangePasswordResponded), - ) -} - -fn create_comment(model: Model) { - let post_id = case model.show_post { - Some(post) -> post.id |> int.to_string - None -> panic as "Invalid state" - } - - lustre_http.post( - env.get_api_url() <> "/api/posts/" <> post_id <> "/comments", - json.object([ - #("body", json.string(model.create_comment_body)), - #("parent_id", case model.create_comment_parent_id { - Some(parent_id) -> json.int(parent_id) - None -> json.null() - }), - ]), - lustre_http.expect_json(message_error_decoder(), CreateCommentResponded), - ) -} - -pub fn view(model: Model) -> Element(Msg) { - body( - [ - class( - "bg-[#fefefc] text-[#151515] w-[100vw] min-h-[100vh] h-[100vh] px-4 max-w-[800px] py-4 mx-auto flex flex-col h-screen gap-4", - ), - id("app"), - ], - [ - nav( - [ - class( - "text-sm font-bold text-neutral-700 h-[28px] flex justify-between items-center", - ), - ], - [ - a([href("/"), class("flex group gap-2 items-center")], [ - img([ - src("https://gleam.run/images/lucy/lucy.svg"), - attribute.alt("Lucy"), - class("size-[18px] group-hover:animate-wiggle"), - ]), - p([class("group-hover:underline")], [text("Latest")]), - ]), - case model.auth_user { - None -> - a([href("/auth/login"), class("hover:underline")], [text("Login")]) - Some(auth_user) -> - span([class("flex gap-2 items-center")], [ - a( - [ - class("font-normal"), - button_class(), - class("hover:bg-[#584355]/80"), - href("/create-post"), - ], - [text("Post")], - ), - a( - [ - class("hover:underline"), - href("/user/" <> auth_user.username), - ], - [text(auth_user.username)], - ), - ]) - }, - ], - ), - html.main([class("mb-auto")], [ - case model.route, model.auth_user { - Active, _ -> latest_view(model) - Login, _ -> login_view(model) - Signup(auth_code), _ -> signup_view(model, auth_code) - ForgotPassword, _ -> forgot_password(model) - ChangePassword(_), _ -> change_password(model) - CreatePost, Some(_) -> create_post_view(model) - ShowPost(_), _ -> show_post_view(model) - UserPage(_), Some(_) -> user_view(model) - NotFound, _ -> text("404 Not found") - _, _ -> text("404 Not found") - }, - ]), - footer([class("text-center text-neutral-500 text-xs pb-2")], [ - text("Made with <3 by the community, contribute on "), - a( - [ - href("https://github.com/dinkelspiel/kirakira"), - class("hover:underline text-neutral-700 font-bold"), - ], - [text("GitHub")], - ), - ]), - ], - ) -} diff --git a/apps/my-app/client/src/client/state.gleam b/apps/my-app/client/src/client/state.gleam @@ -1,51 +0,0 @@ -import gleam/dynamic -import gleam/option.{type Option} -import lustre_http -import shared.{type Post} - -pub type Route { - Active - Login - Signup(auth_code: String) - ForgotPassword - ChangePassword(token: String) - CreatePost - UserPage(username: String) - ShowPost(post_id: Int) - NotFound -} - -pub type Model { - Model( - route: Route, - inviter: String, - auth_user: Option(AuthUser), - sign_up_username: String, - sign_up_email: String, - sign_up_password: String, - sign_up_error: Option(String), - login_email_username: String, - login_password: String, - login_error: Option(String), - create_post_title: String, - create_post_href: String, - create_post_body: String, - create_post_original_creator: Bool, - create_post_use_body: Bool, - create_post_tags: List(Int), - create_post_error: Option(String), - posts: List(Post), - show_post: Option(Post), - create_comment_body: String, - create_comment_error: Option(String), - create_comment_parent_id: Option(Int), - tags: List(Tag), - invite_link: Option(String), - forgot_password_response: Option(Result(String, String)), - change_password_target: String, - ) -} - -pub type Msg { - LoadPosts -} diff --git a/apps/my-app/server/.github/workflows/test.yml b/apps/my-app/server/.github/workflows/test.yml @@ -1,23 +0,0 @@ -name: test - -on: - push: - branches: - - master - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - with: - otp-version: "26.0.2" - gleam-version: "1.1.0" - rebar3-version: "3" - # elixir-version: "1.15.4" - - run: gleam deps download - - run: gleam test - - run: gleam format --check src test diff --git a/apps/my-app/server/.gitignore b/apps/my-app/server/.gitignore @@ -1,8 +0,0 @@ -*.beam -*.ez -/build -erl_crash.dump -/priv - -node_modules/ - diff --git a/apps/my-app/server/gleam.toml b/apps/my-app/server/gleam.toml @@ -1,39 +0,0 @@ -name = "server" -version = "1.0.0" -target = "erlang" - -# Fill out these fields if you intend to generate HTML documentation or publish -# your project to the Hex package manager. -# -# description = "" -# licences = ["Apache-2.0"] -# repository = { type = "github", user = "username", repo = "project" } -# links = [{ title = "Website", href = "https://gleam.run" }] -# -# For a full reference of all the available options, you can have a look at -# https://gleam.run/writing-gleam/gleam-toml/. - -[dependencies] -gleam_stdlib = ">= 0.40.0 and < 1.0.0" -gleam_erlang = ">= 0.25.0 and < 1.0.0" -gleam_json = ">= 1.0.1 and < 2.0.0" -gleam_http = ">= 3.6.0 and < 4.0.0" -gleam_otp = ">= 0.12.0 and < 1.0.0" -gleam_pgo = ">= 0.14.0 and < 1.0.0" -mist = ">= 2.0.0 and < 3.0.0" -lustre = ">= 4.3.6 and < 5.0.0" -wisp = ">= 1.1.0 and < 2.0.0" -glenv = ">= 0.4.0 and < 1.0.0" -birl = ">= 1.7.1 and < 2.0.0" -decode = ">= 0.2.0 and < 1.0.0" -shared = { path = "../shared" } - -[dev-dependencies] -gleeunit = ">= 1.2.0 and < 2.0.0" -gleescript = ">= 1.4.0 and < 2.0.0" - -[erlang] -# Starting an SSL connection relies on ssl application to be started. -# Add pgo application to make sure it's started before querying postgres. -extra_applications = ["ssl"] - diff --git a/apps/my-app/server/manifest.toml b/apps/my-app/server/manifest.toml @@ -1,60 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, - { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "decode", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "decode", source = "hex", outer_checksum = "965F517F67B8C172CA27A5C8E34C73733139E8C9E64736181B8C3179281F9793" }, - { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, - { name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.12.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BFACC1513410DF5A1617169A9CD7EA334973AC71D860A17574BA7B2EADD89A6F" }, - { name = "gleam_pgo", version = "0.14.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "pgo"], otp_app = "gleam_pgo", source = "hex", outer_checksum = "12256A7F351E994A6E43AB67276DD8AACE752991C013F48A288C4A848F3A9758" }, - { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, - { name = "gleescript", version = "1.4.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "gleam_erlang", "gleam_stdlib", "simplifile", "snag", "tom"], otp_app = "gleescript", source = "hex", outer_checksum = "8CDDD29F91064E69950A91A40061785F10275ADB70A0520075591F61A724C455" }, - { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, - { name = "glenv", version = "0.4.0", build_tools = ["gleam"], requirements = ["decode", "envoy", "gleam_stdlib"], otp_app = "glenv", source = "hex", outer_checksum = "46164B9FFEB08927FD2CEBD96C3AFBE082AD5CC2C6F2FC4A78EFDA9EB61E3510" }, - { name = "glisten", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "48EF7F6D1DCA877C2F49AF35CC33946C7129EEB05A114758A2CC569C708BFAF8" }, - { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, - { name = "lustre", version = "4.3.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "A122A11C761E2366EEA4649CBBE5C162BCA8EF5D78C0B1C7AACBA6E824AE9F5D" }, - { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, - { name = "mist", version = "2.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "981F12FC8BA0656B40099EC876D6F2BEE7B95593610F342E9AB0DC4E663A932F" }, - { name = "opentelemetry_api", version = "1.3.0", build_tools = ["rebar3", "mix"], requirements = ["opentelemetry_semantic_conventions"], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "B9E5FF775FD064FA098DBA3C398490B77649A352B40B0B730A6B7DC0BDD68858" }, - { name = "opentelemetry_semantic_conventions", version = "0.2.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_semantic_conventions", source = "hex", outer_checksum = "D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895" }, - { name = "pg_types", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB" }, - { name = "pgo", version = "0.14.0", build_tools = ["rebar3"], requirements = ["backoff", "opentelemetry_api", "pg_types"], otp_app = "pgo", source = "hex", outer_checksum = "71016C22599936E042DC0012EE4589D24C71427D266292F775EBF201D97DF9C9" }, - { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], source = "local", path = "../shared" }, - { name = "simplifile", version = "2.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "BDD04F5D31D6D34E2EDFAEF0B68A6297AEC939888C3BFCE61133DE13857F6DA2" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "9EECB60150E834A07238BD5C7DF1FF07F7D4C5862BB8A773923D1981C7875FB0" }, - { name = "wisp", version = "1.1.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "5B25E37E08A94039668D2ACF7B2D2A665BEAF4B48EFA8613010B4E3164137664" }, -] - -[requirements] -birl = { version = ">= 1.7.1 and < 2.0.0" } -decode = { version = ">= 0.2.0 and < 1.0.0" } -gleam_erlang = { version = ">= 0.25.0 and < 1.0.0" } -gleam_http = { version = ">= 3.6.0 and < 4.0.0" } -gleam_json = { version = ">= 1.0.1 and < 2.0.0" } -gleam_otp = { version = ">= 0.12.0 and < 1.0.0" } -gleam_pgo = { version = ">= 0.14.0 and < 1.0.0" } -gleam_stdlib = { version = ">= 0.40.0 and < 1.0.0" } -gleescript = { version = ">= 1.4.0 and < 2.0.0" } -gleeunit = { version = ">= 1.2.0 and < 2.0.0" } -glenv = { version = ">= 0.4.0 and < 1.0.0" } -lustre = { version = ">= 4.3.6 and < 5.0.0" } -mist = { version = ">= 2.0.0 and < 3.0.0" } -shared = { path = "../shared" } -wisp = { version = ">= 1.1.0 and < 2.0.0" } diff --git a/apps/my-app/server/src/erlib.erl b/apps/my-app/server/src/erlib.erl @@ -1,7 +0,0 @@ --module(erlib). --export([process_message_queue_len/0]). - -process_message_queue_len() -> - % Use process_info/2 to get the message_queue_len of the current process - {message_queue_len, Len} = process_info(self(), message_queue_len), - Len. diff --git a/apps/my-app/server/src/server.gleam b/apps/my-app/server/src/server.gleam @@ -1,38 +0,0 @@ -import gleam/erlang/process -import mist -import server/actors/room_actor -import server/db -import server/env -import server/router -import server/web -import wisp - -pub fn main() { - // This sets the logger to print INFO level logs, and other sensible defaults - // for a web application. - wisp.configure_logger() - - // // Here we generate a secret key, but in a real application you would want to - // // load this from somewhere so that it is not regenerated on every restart. - // let secret_key_base = wisp.random_string(64) - - let env = env.get_env() - let ctx = - web.Context( - connection: db.get_connection(env), - env: env, - room_actor: room_actor.start(), - ) - - // Start the Mist web server. - let assert Ok(_) = - router.handle_request(_, ctx) - // wisp.mist_handler(router.handle_request(_, ctx), secret_key_base) - |> mist.new - |> mist.port(ctx.env.port) - |> mist.start_http - - // The web server runs in new Erlang process, so put this one to sleep while - // it works concurrently. - process.sleep_forever() -} diff --git a/apps/my-app/server/src/server/actors.gleam b/apps/my-app/server/src/server/actors.gleam @@ -1,12 +0,0 @@ -import gleam/erlang/process.{type Subject} -import shared.{type Message} - -pub type CustomWebsocketMessage { - SendToClient(message: Message) -} - -pub type RoomActorMessage { - ConnectUser(user_subject: Subject(CustomWebsocketMessage)) - DisconnectUser(user_subject: Subject(CustomWebsocketMessage)) - SendToAll(message: Message) -} diff --git a/apps/my-app/server/src/server/actors/calculator_actor.gleam b/apps/my-app/server/src/server/actors/calculator_actor.gleam @@ -1,72 +0,0 @@ -// an example of a cpu intensive actor -// supports: factorial 5! = 5 * 4 * 3 * 2 * 1 = 120 - -import gleam/erlang/process.{type Subject} -import gleam/int -import gleam/list -import gleam/otp/actor.{type Next} -import gleam/regex -import gleam/result -import gleam/string -import wisp - -pub type CalculatorActorMessage { - // Subjects are generic over their message type, and `Result(String, Nil)` is the type - // we want our public `solve` function to return. - Solve(reply_to: Subject(Result(String, Nil)), calculation: String) - Shutdown -} - -pub fn is_calculation(content: String) -> Bool { - // regex {number}! or {number}!! - let assert Ok(input_pattern) = regex.from_string("^\\d+!+$") - regex.check(input_pattern, content) -} - -pub fn start() -> Subject(CalculatorActorMessage) { - wisp.log_info("Starting calculator actor") - - let state = Nil - let assert Ok(actor) = actor.start(state, handle_message) - - actor -} - -fn handle_message( - message: CalculatorActorMessage, - _state: Nil, -) -> Next(CalculatorActorMessage, Nil) { - case message { - Shutdown -> actor.Stop(process.Normal) - Solve(client, calculation) -> { - let number = - calculation - |> string.split("!") - |> list.first - |> result.unwrap("") - |> int.parse - - let result = case number { - Ok(number) -> { - let result = number |> factorial(1) |> int.to_string - wisp.log_info("Calculation inner finished!") - Ok(result) - } - _ -> { - wisp.log_error("Invalid calculation") - Error(Nil) - } - } - - process.send(client, result) - actor.continue(Nil) - } - } -} - -fn factorial(n: Int, acc: Int) -> Int { - case n { - 0 -> acc - _ -> factorial(n - 1, n * acc) - } -} diff --git a/apps/my-app/server/src/server/actors/room_actor.gleam b/apps/my-app/server/src/server/actors/room_actor.gleam @@ -1,80 +0,0 @@ -import gleam/erlang/process.{type Subject} -import gleam/int -import gleam/list -import gleam/otp/actor.{type Next} -import gleam/string -import server/actors.{ - type CustomWebsocketMessage, type RoomActorMessage, ConnectUser, - DisconnectUser, SendToAll, SendToClient, -} -import shared.{type Message, RoomUpdate} -import wisp - -pub type RoomActorState { - RoomActorState(participants: List(#(String, Subject(CustomWebsocketMessage)))) -} - -pub fn start() -> Subject(RoomActorMessage) { - wisp.log_info("Starting room actor") - - let state = RoomActorState(participants: []) - let assert Ok(actor) = actor.start(state, handle_message) - - actor -} - -fn handle_message( - message: RoomActorMessage, - state: RoomActorState, -) -> Next(RoomActorMessage, RoomActorState) { - case message { - ConnectUser(user_subject) -> { - let new_participant = #("User", user_subject) - let new_participants = list.append(state.participants, [new_participant]) - let num_participants = list.length(new_participants) - - let new_state = RoomActorState(participants: new_participants) - - send_to_all(new_participants, RoomUpdate(num_participants)) - - wisp.log_info( - "User joined! num participants: " <> num_participants |> int.to_string, - ) - - new_state |> actor.continue - } - DisconnectUser(user_subject) -> { - let new_participants = - list.filter(state.participants, fn(p) { p.1 != user_subject }) - let new_state = RoomActorState(participants: new_participants) - let num_participants = list.length(new_participants) - - send_to_all(new_participants, RoomUpdate(num_participants)) - - wisp.log_info( - "User left! num participants: " <> num_participants |> int.to_string, - ) - - new_state |> actor.continue - } - SendToAll(message) -> { - send_to_all(state.participants, message) - - state |> actor.continue - } - } -} - -fn send_to_all( - participants: List(#(String, Subject(CustomWebsocketMessage))), - message: Message, -) { - let num_participants = list.length(participants) - wisp.log_info( - "Sending to " - <> num_participants |> int.to_string - <> " participants: " - <> string.slice(shared.message_to_string(message), 0, 200), - ) - list.each(participants, fn(p) { process.send(p.1, SendToClient(message)) }) -} diff --git a/apps/my-app/server/src/server/actors/websocket_actor.gleam b/apps/my-app/server/src/server/actors/websocket_actor.gleam @@ -1,191 +0,0 @@ -import birl -import gleam/erlang/process.{type Subject, Normal} -import gleam/function -import gleam/http/request.{type Request} -import gleam/http/response.{type Response} -import gleam/int -import gleam/option.{type Option, None, Some} -import gleam/otp/actor.{type Next, Stop} -import gleam/string -import mist.{ - type Connection, type ResponseData, type WebsocketConnection, - type WebsocketMessage, Custom, Text, -} -import server/actors.{ - type CustomWebsocketMessage, type RoomActorMessage, ConnectUser, - DisconnectUser, SendToAll, SendToClient, -} -import server/actors/calculator_actor.{Solve, is_calculation} -import server/process_utils.{process_message_queue_len} -import shared.{ChatMessage} -import wisp - -pub type WebsocketActorState { - WebsocketActorState( - name: Option(String), - ws_subject: Subject(CustomWebsocketMessage), - room_subject: Option(Subject(RoomActorMessage)), - ) -} - -pub fn start( - req: Request(Connection), - room_subject: Subject(RoomActorMessage), -) -> Response(ResponseData) { - mist.websocket( - request: req, - on_init: fn(_) { - let ws_subject = process.new_subject() - let new_selector = - process.new_selector() - |> process.selecting(ws_subject, function.identity) - - let state = - WebsocketActorState( - name: None, - ws_subject: ws_subject, - room_subject: Some(room_subject), - ) - - // register at room - process.send(room_subject, ConnectUser(ws_subject)) - - #(state, Some(new_selector)) - }, - on_close: fn(state) { - wisp.log_info("A connection was closed") - state |> cleanup - Nil - }, - handler: handle_message, - ) -} - -pub fn handle_message( - state: WebsocketActorState, - connection: WebsocketConnection, - message: WebsocketMessage(CustomWebsocketMessage), -) -> Next(CustomWebsocketMessage, WebsocketActorState) { - case message { - // from internal - Custom(message) -> - case message { - SendToClient(message) -> { - // stringify to json - let message_json = shared.message_to_string(message) - let assert Ok(_) = mist.send_text_frame(connection, message_json) - state |> actor.continue - } - } - // from browser - Text(message) -> { - // check queue length of process - // if too high this means client is sending faster than we can process - // we stop calculations if > 3 - // in testing it was discovered that this number increases by 1 between - // runs of handle_message. So our view of how much work is queued is - // delayed. - let process_queue_len = process_message_queue_len() - case process_queue_len { - i if i > 3 -> { - wisp.log_warning("queue len:" <> process_queue_len |> int.to_string) - } - _ -> Nil - } - - // parse from json - let parsed_message = case message |> shared.message_from_string { - Ok(parsed_message) -> { - parsed_message - } - Error(_) -> { - wisp.log_error("Failed to parse message: " <> message) - let now = birl.now() - ChatMessage( - "Failed to parse message: " <> message, - "System", - birl.to_iso8601(now), - ) - } - } - - // transform message (calculator) - // forward ChatMessage to room - case parsed_message { - ChatMessage(content, author, created_at) -> { - // calculator (sync) - let parsed_message = case - is_calculation(content) && process_queue_len <= 3 - { - False -> parsed_message - True -> { - // start new actor - let calc_actor = calculator_actor.start() - - // call actor with timeout ms - let calc_result = - process.try_call( - calc_actor, - fn(reply_to) { Solve(reply_to, content) }, - 2000, - ) - - // stop actor - // without the unlink we kill (our)self - process.unlink(process.subject_owner(calc_actor)) - process.kill(process.subject_owner(calc_actor)) - - case calc_result { - Ok(Ok(calc_result)) -> { - ChatMessage( - content <> " = " <> calc_result, - author, - created_at, - ) - } - Error(e) -> { - let error = string.inspect(e) - ChatMessage( - content <> " = ? (" <> error <> ")", - author, - created_at, - ) - } - _ -> parsed_message - } - } - } - - // send to room - { - use room_subject <- option.then(state.room_subject) - Some(process.send(room_subject, SendToAll(parsed_message))) - } - } - _ -> { - wisp.log_error("Received message is not a ChatMessage: " <> message) - None - } - } - - state |> actor.continue - } - _ -> { - wisp.log_info("Stopping websocket actor in handle_message") - cleanup(state) - Stop(Normal) - } - } -} - -fn cleanup(state: WebsocketActorState) -> WebsocketActorState { - case state.room_subject { - Some(room_subject) -> { - process.send(room_subject, DisconnectUser(state.ws_subject)) - WebsocketActorState(..state, room_subject: None) - } - None -> state - } - - state -} diff --git a/apps/my-app/server/src/server/db.gleam b/apps/my-app/server/src/server/db.gleam @@ -1,24 +0,0 @@ -import gleam/option.{Some} -import gleam/pgo -import server/env.{type Env} - -pub fn get_connection(env: Env) { - let use_ssl = case env.db_host { - "127.0.0.1" -> False - _ -> True - } - - let connection = - pgo.Config( - ..pgo.default_config(), - host: env.db_host, - port: env.db_port, - user: env.db_user, - password: Some(env.db_password), - database: env.db_name, - ssl: use_ssl, - ) - |> pgo.connect - - connection -} diff --git a/apps/my-app/server/src/server/db/datetime_helper.gleam b/apps/my-app/server/src/server/db/datetime_helper.gleam @@ -1,31 +0,0 @@ -import birl.{type Time} -import gleam/dynamic.{type DecodeError, type Dynamic} -import gleam/float -import gleam/result - -// pub fn convert_time(time: Time) { -// time -// |> birl.to_erlang_universal_datetime() -// |> dynamic.from() -// // |> dynamic.unsafe_coerce() -// } - -pub fn decode_time(data: Dynamic) -> Result(Time, List(DecodeError)) { - data - |> dynamic.tuple2(decode_time_tuple, decode_time_tuple) - |> result.map(birl.from_erlang_universal_datetime) -} - -fn decode_time_tuple(data: Dynamic) { - dynamic.tuple3( - dynamic.int, - dynamic.int, - dynamic.any([dynamic.int, rounded_float]), - )(data) -} - -fn rounded_float(data: Dynamic) { - data - |> dynamic.float() - |> result.map(float.round) -} diff --git a/apps/my-app/server/src/server/db/login.gleam b/apps/my-app/server/src/server/db/login.gleam @@ -1,68 +0,0 @@ -import gleam/dynamic -import gleam/pgo - -pub fn create_user( - db: pgo.Connection, - username: String, - email: String, - password: String, -) -> Result(Nil, pgo.QueryError) { - // ) -> Result(String, pgo.QueryError) { - let query = - " - INSERT INTO twitter.users (username, email, password_hash) - VALUES ($1, $2, CRYPT($3, GEN_SALT('md5'))); - " - let response = - pgo.execute( - query, - db, - [pgo.text(username), pgo.text(email), pgo.text(password)], - dynamic.element(0, dynamic.string), - ) - case response { - Ok(pgo.Returned(1, [])) -> Ok(Nil) - Error(e) -> Error(e) - _ -> panic - } -} - -pub type LoginError { - IncorrectPassword - UserNotFound(user: String) - QueryError(pgo.QueryError) -} - -pub fn log_in( - db: pgo.Connection, - username: String, - password: String, -) -> Result(Nil, LoginError) { - let query = - " - SELECT (password_hash = CRYPT($2, password_hash)) AS password_matches - FROM twitter.users - WHERE username = $1; - " - let response = - pgo.execute( - query, - db, - [pgo.text(username), pgo.text(password)], - dynamic.element(0, dynamic.bool), - ) - - case response { - Ok(returned) -> - case returned { - pgo.Returned(1, [result]) -> - case result { - True -> Ok(Nil) - False -> Error(IncorrectPassword) - } - pgo.Returned(0, []) -> Error(UserNotFound(username)) - _ -> panic - } - Error(e) -> Error(QueryError(e)) - } -} diff --git a/apps/my-app/server/src/server/db/post.gleam b/apps/my-app/server/src/server/db/post.gleam @@ -1,30 +0,0 @@ -import gleam/dynamic.{type DecodeError, type Dynamic} -import gleam/pgo -import gleam/result -import server/db/datetime_helper -import shared - -fn decode_post(data: Dynamic) -> Result(shared.Post, List(DecodeError)) { - data - |> dynamic.tuple4( - dynamic.int, - dynamic.int, - dynamic.string, - datetime_helper.decode_time, - ) - |> result.map(fn(x) { - shared.Post(id: x.0, user_id: x.1, content: x.2, created_at: x.3) - }) -} - -pub fn posts( - db: pgo.Connection, -) -> Result(pgo.Returned(shared.Post), pgo.QueryError) { - let query = - " - SELECT id, user_id, content, created_at - FROM twitter.tweets - ORDER BY created_at DESC; - " - pgo.execute(query, db, [], decode_post) -} diff --git a/apps/my-app/server/src/server/env.gleam b/apps/my-app/server/src/server/env.gleam @@ -1,46 +0,0 @@ -import decode -import glenv - -pub type Env { - Env( - port: Int, - db_host: String, - db_password: String, - db_port: Int, - db_user: String, - db_name: String, - ) -} - -pub fn get_env() { - let definitions = [ - #("PORT", glenv.Int), - #("PGHOST", glenv.String), - #("PGPASSWORD", glenv.String), - #("PGPORT", glenv.Int), - #("PGUSER", glenv.String), - #("PGDB", glenv.String), - ] - - let decoder = - decode.into({ - use port <- decode.parameter - use db_host <- decode.parameter - use db_password <- decode.parameter - use db_port <- decode.parameter - use db_user <- decode.parameter - use db_name <- decode.parameter - - Env(port:, db_host:, db_password:, db_port:, db_user:, db_name:) - }) - |> decode.field("PORT", decode.int) - |> decode.field("PGHOST", decode.string) - |> decode.field("PGPASSWORD", decode.string) - |> decode.field("PGPORT", decode.int) - |> decode.field("PGUSER", decode.string) - |> decode.field("PGDB", decode.string) - - let assert Ok(env) = glenv.load(decoder, definitions) - - env -} diff --git a/apps/my-app/server/src/server/process_utils.gleam b/apps/my-app/server/src/server/process_utils.gleam @@ -1,4 +0,0 @@ -// see erlib.erl for the implementation - -@external(erlang, "erlib", "process_message_queue_len") -pub fn process_message_queue_len() -> Int diff --git a/apps/my-app/server/src/server/response.gleam b/apps/my-app/server/src/server/response.gleam @@ -1,23 +0,0 @@ -import gleam/json -import gleam/string_builder.{type StringBuilder} -import wisp - -pub fn generate_wisp_response(result: Result(StringBuilder, String)) { - case result { - Ok(json) -> wisp.json_response(json, 200) - Error(error) -> - wisp.json_response( - json.object([#("error", json.string(error))]) - |> json.to_string_builder, - 200, - ) - } -} - -pub fn error(error: String) { - wisp.json_response( - json.object([#("error", json.string(error))]) - |> json.to_string_builder, - 400, - ) -} diff --git a/apps/my-app/server/src/server/router.gleam b/apps/my-app/server/src/server/router.gleam @@ -1,339 +0,0 @@ -import gleam/bytes_builder -import gleam/http/request.{type Request} -import gleam/http/response.{type Response} -import gleam/int -import gleam/list -import gleam/pgo -import gleam/string_builder -import lustre/attribute.{attribute, class, href, id, name, rel, src} -import lustre/element.{type Element, text} -import lustre/element/html.{ - a, body, div, footer, head, html, img, li, link, meta, nav, ol, p, script, - section, span, title, -} -import mist.{type Connection, type ResponseData} -import server/actors/websocket_actor -import server/db/post -import server/routes/posts -import server/web -import shared - -pub type Model { - Model(posts: List(shared.Post)) -} - -pub type Message { - LoadPosts -} - -pub fn handle_request( - req: Request(Connection), - ctx: web.Context, -) -> Response(ResponseData) { - // use req <- web.middleware(req) - - // NOTE: assets under /static are caught by web.middleware before this - let segments = request.path_segments(req) - case segments { - ["posts"] -> { - let posts = case post.posts(ctx.connection) { - Ok(returned) -> - case returned { - pgo.Returned(_count, rows) -> rows - } - Error(e) -> - case e { - pgo.ConstraintViolated(message, _constraint, _detail) -> - panic as message - pgo.PostgresqlError(_code, _name, message) -> panic as message - // UnexpectedArgumentCount(expected, got) -> panic as - // UnexpectedArgumentType(expected, got) - pgo.UnexpectedResultType(_err) -> panic as "unexpected result" - pgo.ConnectionUnavailable -> panic as "connection unavailable" - _ -> panic - } - } - let model = Model(posts:) - // let html = - // html([], [ - // html.head([], [ - // html.script([attribute.type_("module"), attribute.src("...")], ""), - // html.script( - // [attribute.type_("application/json"), attribute.id("model")], - // json.int(model) - // |> json.to_string, - // ), - // ]), - // html.body([], [html.div([attribute.id("app")], [counter.view(model)])]), - // ]) - response.new(200) - |> response.set_header("Content-Type", "text/html") - |> response.set_body( - view(model) - |> page_scaffold - |> element.to_document_string_builder() - |> string_builder.to_string - |> bytes_builder.from_string - |> mist.Bytes, - ) - } - ["ws"] -> websocket_actor.start(req, ctx.room_actor) - ["api", "posts"] -> posts.posts(req, ctx) - _ -> { - let empty_body = mist.Bytes(bytes_builder.new()) - let not_found = response.new(404) |> response.set_body(empty_body) - not_found - } - } -} - -pub fn page_scaffold(content: element.Element(a)) { - html([], [ - head([], [ - meta([attribute("charset", "UTF-8")]), - meta([ - attribute("content", "width=device-width, initial-scale=1.0"), - name("viewport"), - ]), - title([], "📋 Kirakira"), - meta([ - attribute("content", "A forum made in Gleam for the Gleam community"), - name("description"), - ]), - meta([attribute("content", "max-image-preview:large"), name("robots")]), - meta([attribute("content", "en_US"), attribute("property", "og:locale")]), - meta([ - attribute("content", "📋 Kirakira"), - attribute("property", "og:site_name"), - ]), - meta([attribute("content", "website"), attribute("property", "og:type")]), - meta([ - attribute("content", "📋 Kirakira"), - attribute("property", "og:title"), - ]), - meta([ - attribute("content", "A forum made in Gleam for the Gleam community"), - attribute("property", "og:description"), - ]), - meta([ - attribute("content", "https://kirakira.keii.dev/"), - attribute("property", "og:url"), - ]), - meta([attribute("content", "summary"), name("twitter:card")]), - meta([attribute("content", "📋 Kirakira"), name("twitter:title")]), - meta([ - attribute("content", "A forum made in Gleam for the Gleam community"), - name("twitter:description"), - ]), - link([ - href("/priv/static/favicon.ico"), - attribute.type_("image/x-icon"), - rel("icon"), - ]), - link([href("/static/client.min.css"), rel("stylesheet")]), - script([src("/static/client.min.mjs"), attribute.type_("module")], ""), - script( - [ - src("https://plausible.keii.dev/js/script.js"), - attribute("data-domain", "kirakira.keii.dev"), - attribute("defer", ""), - ], - "", - ), - ]), - content, - ]) -} - -pub fn view(model: Model) -> Element(Message) { - body( - [ - class( - "bg-[#fefefc] text-[#151515] w-[100vw] min-h-[100vh] h-[100vh] px-4 max-w-[800px] py-4 mx-auto flex flex-col h-screen gap-4", - ), - id("app"), - ], - [ - // title([], "Hello World"), - nav( - [ - class( - "text-sm font-bold text-neutral-700 h-[28px] flex justify-between items-center", - ), - ], - [ - a([href("/"), class("flex group gap-2 items-center")], [ - // img([ - // src("https://gleam.run/images/lucy/lucy.svg"), - // attribute.alt("Lucy"), - // class("size-[18px] group-hover:animate-wiggle"), - // ]), - p([class("group-hover:underline")], [text("Latest")]), - ]), - ], - ), - html.main([class("mb-auto")], [ - view_posts(model), - // case model.route, model.auth_user { - // Active, _ -> latest_view(model) - // Login, _ -> login_view(model) - // Signup(auth_code), _ -> signup_view(model, auth_code) - // ForgotPassword, _ -> forgot_password(model) - // ChangePassword(_), _ -> change_password(model) - // CreatePost, Some(_) -> create_post_view(model) - // ShowPost(_), _ -> show_post_view(model) - // UserPage(_), Some(_) -> user_view(model) - // NotFound, _ -> text("404 Not found") - // _, _ -> text("404 Not found") - // }, - ]), - footer([class("text-center text-neutral-500 text-xs pb-2")], [ - text("Made with <3 by the community, contribute on "), - a( - [ - href("https://github.com/dinkelspiel/kirakira"), - class("hover:underline text-neutral-700 font-bold"), - ], - [text("GitHub")], - ), - ]), - ], - ) -} - -fn view_post(post: shared.Post) { - li( - [ - class("grid grid-cols-[18px,1fr] gap-2"), - attribute.id("post-" <> int.to_string(post.id)), - ], - [ - div([class("flex flex-col gap-1")], [ - span([class("flex flex-col sm:flex-row sm:gap-2 sm:items-center")], [ - // a( - // [ - // class("text-[#584355] font-bold"), - // // href(case post.href { - // // Some(post_href) -> post_href - // // None -> "/post/" <> { post.id |> int.to_string } - // // }), - // // target("_blank"), - // ], - // [text(post.)], - // ), - a( - [ - attribute.attribute("title", post.content), - class("text-xs text-neutral-500 hidden sm:block"), - ], - [text("☶"), text(post.content)], - ), - ]), - ]), - ], - ) - // div([class("ps-[18px] grid gap-4 pb-4")], [ - // div([class("text-sm")], [text(body)]), - // create_comment_view(model), - // button( - // [ - // class("text-xs text-neutral-500 me-auto"), - // attribute.type_("button"), - // event.on_click(CreateCommentUpdateParentId(None)), - // ], - // [text("Reset reply")], - // ) - // ]), - // ol( - // [attribute.id("comments")], - // comments_view( - // model, - // post.comments - // |> list.sort(fn(a, b) { int.compare(b.likes, a.likes) }), - // None, - // ), - // ), -} - -pub fn view_posts(model: Model) { - section( - [class("grid gap-4"), attribute.id("thread")], - model.posts |> list.map(view_post), - ) -} -// pub fn view(model: Model) -> Element(Msg) { -// body( -// [ -// class( -// "bg-[#fefefc] text-[#151515] w-[100vw] min-h-[100vh] h-[100vh] px-4 max-w-[800px] py-4 mx-auto flex flex-col h-screen gap-4", -// ), -// id("app"), -// ], -// [ -// nav( -// [ -// class( -// "text-sm font-bold text-neutral-700 h-[28px] flex justify-between items-center", -// ), -// ], -// [ -// a([href("/"), class("flex group gap-2 items-center")], [ -// img([ -// src("https://gleam.run/images/lucy/lucy.svg"), -// attribute.alt("Lucy"), -// class("size-[18px] group-hover:animate-wiggle"), -// ]), -// p([class("group-hover:underline")], [text("Latest")]), -// ]), -// case model.auth_user { -// None -> -// a([href("/auth/login"), class("hover:underline")], [text("Login")]) -// Some(auth_user) -> -// span([class("flex gap-2 items-center")], [ -// a( -// [ -// class("font-normal"), -// button_class(), -// class("hover:bg-[#584355]/80"), -// href("/create-post"), -// ], -// [text("Post")], -// ), -// a( -// [ -// class("hover:underline"), -// href("/user/" <> auth_user.username), -// ], -// [text(auth_user.username)], -// ), -// ]) -// }, -// ], -// ), -// html.main([class("mb-auto")], [ -// case model.route, model.auth_user { -// Active, _ -> latest_view(model) -// Login, _ -> login_view(model) -// Signup(auth_code), _ -> signup_view(model, auth_code) -// ForgotPassword, _ -> forgot_password(model) -// ChangePassword(_), _ -> change_password(model) -// CreatePost, Some(_) -> create_post_view(model) -// ShowPost(_), _ -> show_post_view(model) -// UserPage(_), Some(_) -> user_view(model) -// NotFound, _ -> text("404 Not found") -// _, _ -> text("404 Not found") -// }, -// ]), -// footer([class("text-center text-neutral-500 text-xs pb-2")], [ -// text("Made with <3 by the community, contribute on "), -// a( -// [ -// href("https://github.com/dinkelspiel/kirakira"), -// class("hover:underline text-neutral-700 font-bold"), -// ], -// [text("GitHub")], -// ), -// ]), -// ], -// ) -// } diff --git a/apps/my-app/server/src/server/routes/posts.gleam b/apps/my-app/server/src/server/routes/posts.gleam @@ -1,67 +0,0 @@ -import birl -import gleam/bytes_builder -import gleam/http.{Get} -import gleam/http/request.{type Request} -import gleam/http/response.{type Response} -import gleam/json.{type Json} -import gleam/pgo -import gleam/result -import gleam/string_builder -import mist.{type Connection, type ResponseData} -import server/db/post -import server/web -import shared - -pub fn posts( - req: Request(Connection), - ctx: web.Context, -) -> Response(ResponseData) { - case req.method { - Get -> list_posts(req, ctx) - _ -> panic - // _ -> wisp.method_not_allowed([Get]) - // Post -> create_post(req, ctx) - // _ -> wisp.method_not_allowed([Get, Post]) - } -} - -pub fn list_posts( - _req: Request(Connection), - ctx: web.Context, -) -> Response(ResponseData) { - let posts = post.posts(ctx.connection) - - let result = case posts { - Ok(rows) -> - case rows { - pgo.Returned(_count, rows) -> - Ok( - json.object([ - #( - "posts", - rows - |> json.array(fn(post) { post_to_json(post) }), - ), - ]) - |> json.to_string_builder - |> string_builder.to_string, - ) - // _ -> Error("Unknown error occurred.") - } - Error(_error) -> Error("Query error!") - } - response.new(200) - |> response.set_body( - result |> result.unwrap_both() |> bytes_builder.from_string |> mist.Bytes, - ) - // response.generate_wisp_response(result) -} - -fn post_to_json(post: shared.Post) -> Json { - json.object([ - #("id", json.int(post.id)), - #("title", json.int(post.user_id)), - #("content", json.string(post.content)), - #("created_at", json.string(birl.to_time_string(post.created_at))), - ]) -} diff --git a/apps/my-app/server/src/server/scaffold.gleam b/apps/my-app/server/src/server/scaffold.gleam @@ -1,61 +0,0 @@ -import lustre/attribute.{attribute, href, name, rel, src} -import lustre/element -import lustre/element/html.{head, html, link, meta, script, title} - -pub fn page_scaffold(content: element.Element(a)) { - html([], [ - head([], [ - meta([attribute("charset", "UTF-8")]), - meta([ - attribute("content", "width=device-width, initial-scale=1.0"), - name("viewport"), - ]), - title([], "📋 Kirakira"), - meta([ - attribute("content", "A forum made in Gleam for the Gleam community"), - name("description"), - ]), - meta([attribute("content", "max-image-preview:large"), name("robots")]), - meta([attribute("content", "en_US"), attribute("property", "og:locale")]), - meta([ - attribute("content", "📋 Kirakira"), - attribute("property", "og:site_name"), - ]), - meta([attribute("content", "website"), attribute("property", "og:type")]), - meta([ - attribute("content", "📋 Kirakira"), - attribute("property", "og:title"), - ]), - meta([ - attribute("content", "A forum made in Gleam for the Gleam community"), - attribute("property", "og:description"), - ]), - meta([ - attribute("content", "https://kirakira.keii.dev/"), - attribute("property", "og:url"), - ]), - meta([attribute("content", "summary"), name("twitter:card")]), - meta([attribute("content", "📋 Kirakira"), name("twitter:title")]), - meta([ - attribute("content", "A forum made in Gleam for the Gleam community"), - name("twitter:description"), - ]), - link([ - href("/priv/static/favicon.ico"), - attribute.type_("image/x-icon"), - rel("icon"), - ]), - link([href("/static/client.min.css"), rel("stylesheet")]), - script([src("/static/client.min.mjs"), attribute.type_("module")], ""), - script( - [ - src("https://plausible.keii.dev/js/script.js"), - attribute("data-domain", "kirakira.keii.dev"), - attribute("defer", ""), - ], - "", - ), - ]), - content, - ]) -} diff --git a/apps/my-app/server/src/server/web.gleam b/apps/my-app/server/src/server/web.gleam @@ -1,52 +0,0 @@ -import gleam/erlang/process.{type Subject} -import gleam/pgo -import server/actors.{type RoomActorMessage} -import server/env.{type Env} -import wisp - -pub type Context { - Context( - connection: pgo.Connection, - env: Env, - room_actor: Subject(RoomActorMessage), - ) -} - -/// The middleware stack that the request handler uses. The stack is itself a -/// middleware function! -/// -/// Middleware wrap each other, so the request travels through the stack from -/// top to bottom until it reaches the request handler, at which point the -/// response travels back up through the stack. -/// -/// The middleware used here are the ones that are suitable for use in your -/// typical web application. -/// -pub fn middleware( - req: wisp.Request, - handle_request: fn(wisp.Request) -> wisp.Response, -) -> wisp.Response { - // Permit browsers to simulate methods other than GET and POST using the - // `_method` query parameter. - let req = wisp.method_override(req) - - // Log information about the request and response. - use <- wisp.log_request(req) - - // Return a default 500 response if the request handler crashes. - use <- wisp.rescue_crashes - - // Rewrite HEAD requests to GET requests and return an empty body. - use req <- wisp.handle_head(req) - - // Serve build output site files - let assert Ok(priv_directory) = wisp.priv_directory("server") - use <- wisp.serve_static( - req, - under: "/static", - from: priv_directory <> "/static", - ) - - // Handle the request! - handle_request(req) -} diff --git a/apps/my-app/server/test/server_test.gleam b/apps/my-app/server/test/server_test.gleam @@ -1,49 +0,0 @@ -import gleam/option -import gleam/pgo -import gleeunit -import gleeunit/should - -import server/db/login - -pub fn main() { - gleeunit.main() -} - -fn get_test_connection() -> pgo.Connection { - let cfg = - pgo.Config( - ..pgo.default_config(), - host: "db-postgresql-ams3-90424-do-user-17154797-0.j.db.ondigitalocean.com", - database: "defaultdb", - user: "doadmin", - password: option.Some("AVNS_1pjlrBSPkd4w0iFMGiE"), - port: 25_060, - ssl: True, - ) - let db = pgo.connect(cfg) - db -} - -pub fn successful_log_in_test() { - let db = get_test_connection() - let successful = db |> login.log_in("silas", "mypassword") - should.be_ok(successful) -} - -pub fn wrong_password_log_in_test() { - let db = get_test_connection() - let wrong_password = db |> login.log_in("silas", "mypasswodr") - case wrong_password { - Error(login.IncorrectPassword) -> Nil - _ -> panic as "password should be wrong" - } -} - -pub fn missing_user_log_in_test() { - let db = get_test_connection() - let missing_user = db |> login.log_in("silos", "mypasswodr") - case missing_user { - Error(login.UserNotFound(_)) -> Nil - _ -> panic as "user should be missing" - } -} diff --git a/apps/my-app/shared/.github/workflows/test.yml b/apps/my-app/shared/.github/workflows/test.yml @@ -1,23 +0,0 @@ -name: test - -on: - push: - branches: - - master - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - with: - otp-version: "26.0.2" - gleam-version: "1.1.0" - rebar3-version: "3" - # elixir-version: "1.15.4" - - run: gleam deps download - - run: gleam test - - run: gleam format --check src test diff --git a/apps/my-app/shared/.gitignore b/apps/my-app/shared/.gitignore @@ -1,4 +0,0 @@ -*.beam -*.ez -/build -erl_crash.dump diff --git a/apps/my-app/shared/gleam.toml b/apps/my-app/shared/gleam.toml @@ -1,22 +0,0 @@ -name = "shared" -version = "1.0.0" -target = "javascript" - -# Fill out these fields if you intend to generate HTML documentation or publish -# your project to the Hex package manager. -# -# description = "" -# licences = ["Apache-2.0"] -# repository = { type = "github", user = "username", repo = "project" } -# links = [{ title = "Website", href = "https://gleam.run" }] -# -# For a full reference of all the available options, you can have a look at -# https://gleam.run/writing-gleam/gleam-toml/. - -[dependencies] -gleam_stdlib = ">= 0.34.0 and < 2.0.0" -gleam_json = ">= 2.0.0 and < 3.0.0" -birl = ">= 1.7.1 and < 2.0.0" - -[dev-dependencies] -gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/apps/my-app/shared/manifest.toml b/apps/my-app/shared/manifest.toml @@ -1,16 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "gleam_json", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB10B0E7BF44282FB25162F1A24C1A025F6B93E777CCF238C4017E4EEF2CDE97" }, - { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, - { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, -] - -[requirements] -birl = { version = ">= 1.7.1 and < 2.0.0" } -gleam_json = { version = ">= 2.0.0 and < 3.0.0"} -gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } -gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/apps/my-app/shared/shell.nix b/apps/my-app/shared/shell.nix @@ -1,15 +0,0 @@ -let - # pkgs = import <nixpkgs> {}; - pkgs = import (builtins.fetchTarball { - name = "nixos-24.05"; - url = "https://github.com/nixos/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.g z"; - sha256 = "1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; - }) {}; -in pkgs.mkShell { - buildInputs = [ - pkgs.gleam - pkgs.erlang_27 - pkgs.rebar3 - ]; - # imports = ["./test.nix"]; -} diff --git a/apps/my-app/shared/src/shared.gleam b/apps/my-app/shared/src/shared.gleam @@ -1,61 +0,0 @@ -import birl -import gleam/dynamic.{type DecodeError, type Dynamic, DecodeError} -import gleam/json.{type Json} -import gleam/result - -pub type Post { - Post(id: Int, user_id: Int, content: String, created_at: birl.Time) -} - -pub type Message { - ChatMessage(text: String, author: String, created_at: String) - RoomUpdate(num_participants: Int) -} - -pub fn message_to_json(msg: Message) -> Json { - case msg { - ChatMessage(text, author, created_at) -> - json.object([ - #("$", json.string("ChatMessage")), - #("text", json.string(text)), - #("author", json.string(author)), - #("created_at", json.string(created_at)), - ]) - RoomUpdate(num_participants) -> - json.object([ - #("$", json.string("RoomUpdate")), - #("num_participants", json.int(num_participants)), - ]) - } -} - -fn decoder(dynamic: Dynamic) -> Result(Message, List(DecodeError)) { - use tag <- result.then(dynamic.field("$", dynamic.string)(dynamic)) - let decoder = case tag { - "ChatMessage" -> - dynamic.decode3( - ChatMessage, - dynamic.field("text", dynamic.string), - dynamic.field("author", dynamic.string), - dynamic.field("created_at", dynamic.string), - ) - "RoomUpdate" -> - dynamic.decode1( - RoomUpdate, - dynamic.field("num_participants", dynamic.int), - ) - - _ -> fn(_) { Error([DecodeError("Message", tag, ["$"])]) } - } - - decoder(dynamic) -} - -pub fn message_to_string(msg: Message) -> String { - message_to_json(msg) - |> json.to_string -} - -pub fn message_from_string(json: String) -> Result(Message, _) { - json.decode(json, decoder) -} diff --git a/apps/my-app/shared/test/shared_test.gleam b/apps/my-app/shared/test/shared_test.gleam @@ -1,12 +0,0 @@ -import gleeunit -import gleeunit/should - -pub fn main() { - gleeunit.main() -} - -// gleeunit test functions end in `_test` -pub fn hello_world_test() { - 1 - |> should.equal(1) -} diff --git a/apps/my_app/.github/workflows/test.yml b/apps/my_app/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "1.5.1" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/apps/my-app/client/.gitignore b/apps/my_app/.gitignore diff --git a/apps/my_app/README.md b/apps/my_app/README.md @@ -0,0 +1,24 @@ +# my_app + +[![Package Version](https://img.shields.io/hexpm/v/my_app)](https://hex.pm/packages/my_app) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/my_app/) + +```sh +gleam add my_app@1 +``` +```gleam +import my_app + +pub fn main() { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at <https://hexdocs.pm/my_app>. + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/apps/my_app/build.sh b/apps/my_app/build.sh @@ -0,0 +1,6 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i bash -p gleam +(cd shared && gleam clean) \ +&& (cd client && gleam clean && gleam run -m lustre/dev build --outdir=../server/priv/static --minify) \ +&& (cd server && gleam deps download && gleam export erlang-shipment && cp -r ./build/erlang-shipment/ ../) + diff --git a/apps/my_app/flake.lock b/apps/my_app/flake.lock @@ -0,0 +1,77 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nix-gleam": { + "locked": { + "lastModified": 1717670865, + "narHash": "sha256-+dAPiKAwCzlKWtx3aIHXfR6jydh1JyangewqZKJx500=", + "owner": "arnarg", + "repo": "nix-gleam", + "rev": "c69abe0e57a01991654d3de63dcd93a8b91b98ff", + "type": "github" + }, + "original": { + "owner": "arnarg", + "repo": "nix-gleam", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1728492678, + "narHash": "sha256-9UTxR8eukdg+XZeHgxW5hQA9fIKHsKCdOIUycTryeVw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5633bcff0c6162b9e4b5f1264264611e950c8ec7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nix-gleam": "nix-gleam", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/apps/my_app/flake.nix b/apps/my_app/flake.nix @@ -0,0 +1,29 @@ +{ + description = "My gleam monorepo"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + inputs.nix-gleam.url = "github:arnarg/nix-gleam"; + + outputs = { + self, + nixpkgs, + flake-utils, + nix-gleam, + }: ( + flake-utils.lib.eachDefaultSystem + (system: let + pkgs = import nixpkgs { + inherit system; + overlays = [ + nix-gleam.overlays.default + ]; + }; + in { + packages.default = pkgs.buildGleamApplication { + src = ./.; + erlangPackage = pkgs.erlang_27; + }; + }) + ); +} diff --git a/apps/my_app/gleam.toml b/apps/my_app/gleam.toml @@ -0,0 +1,24 @@ +name = "my_app" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.34.0 and < 2.0.0" +mist = ">= 3.0.0 and < 4.0.0" +gleam_erlang = ">= 0.27.0 and < 1.0.0" +gleam_http = ">= 3.7.0 and < 4.0.0" +gleam_otp = ">= 0.12.1 and < 1.0.0" +wisp = ">= 1.2.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/apps/my_app/manifest.toml b/apps/my_app/manifest.toml @@ -0,0 +1,37 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, + { name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" }, + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, + { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, + { name = "gleam_erlang", version = "0.27.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "DE468F676D71B313C6C8C5334425CFCF827837333F8AB47B64D8A6D7AA40185D" }, + { name = "gleam_http", version = "3.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "EA66440C2269F7CED0F6845E5BD0DB68095775D627FA709A841CA78A398D6D56" }, + { name = "gleam_json", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB10B0E7BF44282FB25162F1A24C1A025F6B93E777CCF238C4017E4EEF2CDE97" }, + { name = "gleam_otp", version = "0.12.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BFACC1513410DF5A1617169A9CD7EA334973AC71D860A17574BA7B2EADD89A6F" }, + { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "glisten", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "912132751031473CB38F454120124FFC96AF6B0EA33D92C9C90DB16327A2A972" }, + { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, + { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, + { name = "mist", version = "3.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "CDA1A74E768419235E16886463EC4722EFF4AB3F8D820A76EAD45D7C167D7282" }, + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, + { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, + { name = "wisp", version = "1.2.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "F71265D2F1DE11426535A2FA1DA3B11D2FFB783B116DF9496BC8C41983EBADB4" }, +] + +[requirements] +gleam_erlang = { version = ">= 0.27.0 and < 1.0.0" } +gleam_http = { version = ">= 3.7.0 and < 4.0.0" } +gleam_otp = { version = ">= 0.12.1 and < 1.0.0" } +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +mist = { version = ">= 3.0.0 and < 4.0.0" } +wisp = { version = ">= 1.2.0 and < 2.0.0" } diff --git a/apps/my-app/default.nix b/apps/my_app/module.nix diff --git a/apps/my-app/server/shell.nix b/apps/my_app/shell.nix diff --git a/apps/my_app/src/my_app.gleam b/apps/my_app/src/my_app.gleam @@ -0,0 +1,26 @@ +import gleam/erlang/process +import mist +import my_app/router +import wisp +import wisp/wisp_mist + +pub fn main() { + // This sets the logger to print INFO level logs, and other sensible defaults + // for a web application. + wisp.configure_logger() + + // Here we generate a secret key, but in a real application you would want to + // load this from somewhere so that it is not regenerated on every restart. + let secret_key_base = wisp.random_string(64) + + // Start the Mist web server. + let assert Ok(_) = + wisp_mist.handler(router.handle_request, secret_key_base) + |> mist.new + |> mist.port(8003) + |> mist.start_http + + // The web server runs in new Erlang process, so put this one to sleep while + // it works concurrently. + process.sleep_forever() +} diff --git a/apps/my_app/src/my_app/router.gleam b/apps/my_app/src/my_app/router.gleam @@ -0,0 +1,16 @@ +import gleam/string_builder +import my_app/web +import wisp.{type Request, type Response} + +/// The HTTP request handler- your application! +/// +pub fn handle_request(req: Request) -> Response { + // Apply the middleware stack for this request/response. + use _req <- web.middleware(req) + + // Later we'll use templates, but for now a string will do. + let body = string_builder.from_string("<h1>Hello, Joe!</h1>") + + // Return a 200 OK response with the body and a HTML content type. + wisp.html_response(body, 200) +} diff --git a/apps/my_app/src/my_app/web.gleam b/apps/my_app/src/my_app/web.gleam @@ -0,0 +1,32 @@ +import wisp + +/// The middleware stack that the request handler uses. The stack is itself a +/// middleware function! +/// +/// Middleware wrap each other, so the request travels through the stack from +/// top to bottom until it reaches the request handler, at which point the +/// response travels back up through the stack. +/// +/// The middleware used here are the ones that are suitable for use in your +/// typical web application. +/// +pub fn middleware( + req: wisp.Request, + handle_request: fn(wisp.Request) -> wisp.Response, +) -> wisp.Response { + // Permit browsers to simulate methods other than GET and POST using the + // `_method` query parameter. + let req = wisp.method_override(req) + + // Log information about the request and response. + use <- wisp.log_request(req) + + // Return a default 500 response if the request handler crashes. + use <- wisp.rescue_crashes + + // Rewrite HEAD requests to GET requests and return an empty body. + use req <- wisp.handle_head(req) + + // Handle the request! + handle_request(req) +} diff --git a/apps/my-app/client/test/client_test.gleam b/apps/my_app/test/my_app_test.gleam diff --git a/flake.lock b/flake.lock @@ -0,0 +1,79 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1728492678, + "narHash": "sha256-9UTxR8eukdg+XZeHgxW5hQA9fIKHsKCdOIUycTryeVw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5633bcff0c6162b9e4b5f1264264611e950c8ec7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1728156290, + "narHash": "sha256-uogSvuAp+1BYtdu6UWuObjHqSbBohpyARXDWqgI12Ss=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "17ae88b569bb15590549ff478bab6494dde4a907", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1728093190, + "narHash": "sha256-CAZF2NRuHmqTtRTNAruWpHA43Gg2UvuCNEIzabP0l6M=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e2f08f4d8b3ecb5cf5c9fd9cb2d53bb3c71807da", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "sops-nix": "sops-nix" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": "nixpkgs_2", + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1728345710, + "narHash": "sha256-lpunY1+bf90ts+sA2/FgxVNIegPDKCpEoWwOPu4ITTQ=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "06535d0e3d0201e6a8080dd32dbfde339b94f01b", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix @@ -0,0 +1,112 @@ +{ + inputs = { + # nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + sops-nix.url = "github:Mic92/sops-nix"; + my_app.url = "path:./apps/my_app/flake.nix"; + }; + outputs = + { nixpkgs, sops-nix, ... }: + { + colmena = { + meta = { + # nixpkgs = (import nixos_24_05) { }; + nixpkgs = import nixpkgs { + system = "x86_64-linux"; + }; + }; + + defaults = + { pkgs, ... }: + { + system.stateVersion = "24.05"; # Do not change lightly! + environment.systemPackages = with pkgs; [ + vim + wget + curl + rsync + rclone + zip + unzip + ]; + services.openssh.enable = true; + networking.firewall.allowedTCPPorts = [ 80 ]; + + boot.loader.grub = { + enable = false; + }; + fileSystems."/" = { + device = "/dev/sda1"; + fsType = "ext4"; + }; + }; + + nixos-s-1vcpu-512mb-10gb-ams3-01 = + { pkgs, name, ... }: + { + deployment = { + targetHost = "188.166.127.72"; + targetUser = "root"; + }; + networking.hostName = name; + time.timeZone = "Europe/Amsterdam"; + + imports = [ + ./vpn.nix + sops-nix.nixosModules.sops + # sops_nix + # ../apps/my-app/module.nix + ]; + # security.pki.certificates = [ (builtins.readFile "/run/secrets/ca_certificate") ]; + + sops.defaultSopsFile = ./secrets.yaml; + sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; + sops.secrets.server_wireguard_key = { + restartUnits = [ "wg-quick-wg0.service" ]; + }; + sops.secrets.ca_certificate = { }; + + users.users.silas = { + isNormalUser = true; + description = "Silas Brack"; + home = "/home/silas"; + extraGroups = [ + "networkmanager" + "wheel" + ]; # "keys" + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPYO29H4+yDvJbaoUqTg5V6IpD3CMFlB2F47MCzHNpY silasbrack@gmail.com" # Thinkpad + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqHJMc2SCzL50SO9Flfnhn012p1dmp4Iph8lPQ2aoe/ silasbrack@gmail.com" # Desktop + ]; + packages = with pkgs; [ + neovim + gleam + erlang_27 + rebar3 + ]; + }; + + # services.myapp = { + # enable = true; + # port = 8000; + # }; + + # services.postgres = { + # enable = true; + # }; + + services.nginx = { + enable = true; + virtualHosts."localhost" = { + locations."/" = { + return = "200 'Hello, NixOS!'"; + extraConfig = '' + default_type text/plain; + ''; + }; + }; + }; + }; + }; + }; +} diff --git a/infrastructure/hive.nix b/infrastructure/hive.nix @@ -42,7 +42,7 @@ in { imports = [ ./vpn.nix sops_nix - ../apps/my-app + # ../apps/my-app/module.nix ]; # security.pki.certificates = [ (builtins.readFile "/run/secrets/ca_certificate") ]; @@ -70,10 +70,10 @@ in { ]; }; - services.myapp = { - enable = true; - port = 8000; - }; + # services.myapp = { + # enable = true; + # port = 8000; + # }; # services.postgres = { # enable = true; diff --git a/secrets.yaml b/secrets.yaml @@ -0,0 +1,40 @@ +server_wireguard_key: ENC[AES256_GCM,data:21uPlSLnedqeXxIjS+HhJ0lAD6T/diIkjAAhbrB/a6jSVST9u8YTXWNgZ8w=,iv:pOGONrRuUNS8obDdw6GvEkCO7a+Ym5kOgLH6u+P9QQ0=,tag:cAcDTQ5pU8fV5hDzx+d+5A==,type:str] +ca_certificate: ENC[AES256_GCM,data:NPIus6BbTwTIDS/xpBwHuZIN9yqASucABgQNZmCOML+NKRjTSsSCw6t2ulRoSIuyKvXF48yyCzmVHvqpWizy6UAHfcJML9HztIKdoC8NjNviBEI53D8vmpntoMkqCL9HjCmoPJuPstTRw3DEuhlQjsaBXsQLEqmNmFGoa1lagVkNrIakHfNavD0Sd/Y97m0PthkCcY8Iru1QZYa99mo5YmVz0osTG0c1vr8In6wHA6zPuM7ThmuRia7nwrR/vtskKg42w4TSa7V53QHPabKDr+kyM5KTMMEHqI2HLdx3ac6az6MtadBWsDGj+smpKLpxVpFCvDwrs+xS5aoU140h249TwGSRtOajPYvX5J1/X2u/8wLF9Sfv/IB7wxzd66rqSs5q3EuV8Iw9qOuzO+Rleh5ApahYj401HYj9oankR9RBO2tL3hI2x0Ozn3JMMwQThNYZO2Wysl9z+4Vb5Xt9ndIqvU5OJN2TKj8L5KV1BoBj6rpI5XxSwIn0Ng2yyNpym/dl3eej2nXBtwYdL2grUSIoD3tCjMlsZ8OXFp9GHJ/NHa/zsAIl0geb+VpeYwaRJyRYzyhAYKLTl4mWRiEPaQx46Tt/YJVB1uwBY0RNVavWIXES0LCvs9QeLRizQl0dWlt1FdMC5mhE/1lhoscfF1Zudv5j86omEX9hALOcNWaBPk4WXaDC3tSCggW85HUAiU0agiEnuRD01PQlce45m31ck/d6umKUVdTHsiSzAOzGAU45uLWZS1BWIh0y5E48JsOEI0uy4MyCDcqct5fjr7N22Lnvc6C+H9xRJRgRlf7JH2WqezFT1wHgoVz9Ua5z/AbUWfVzmIY4fbTB8u7opKzC7eRJP+UHi/wXHMDQj86ySrPzWnP3WDUYbRSjeama8kZM/RFdzcDgsOCFd5gAnbn+rjOD6+Tmbts5V50VB6dhX1tQJcvl8dL5PDcLY6QoC1GzKUefASRs2tz7uAFwNFiRZSgAm9hYNU0rOKWXqhiBzgnTHpnnE/723NJGd6o4xT1vs+h7K23v0ZufJ+rbxDdMuwU9blXEsxQvHX93Xt903v+Os//L7bs9ZmQYa/i/b5j+vqzEZgI6Aqf7++9/OQoIPEvMaDY0qmOUb9F8ebGkgIo9184Zkx8sGQTMosCiAS4gt+kCo/+ZwgKvdc7leQs90EeG+hjcSP2rjILoPTD+EUqvknuCWWHZ3rPIGSKNOgs7Ph1IGbFU97mI3YiDuM8fdbyjm0G0L9x1llbZOSkaM3Sk4vsmv2v0zTdKxKihrvFXstWCMHZlpbDTSU6WtLCv1fMf3WNDiaK5S1iFumV/me/qWAcCet5NpOrSTva0eJdUrqE9SC+ljXVYEpQpsnKeyydAy6g+PTbQiQKyU7tHzyTlPzy3d7XsTAaHHEXPWNs8XOqJ/LeZBqoAPTnGZXTEgZjLqkosj1XsANGAO9uxLgokra2aW/jOEFCsreIlOk9Cb3TwUupcAVxDtg3vjFBf+PGTFXGqTXFvpfsXTKZ7R7CViP5+Q6Y+esp6be0ceFYbezIX3z/vTMVa2u1JiibHrJTepiCgz4YwY13UlyaLZpGAFyEO+ymvtXEWipyoSvz+DlMpHN+97TX18//Q0YLjSqiLJ61TbAnnsJ/Ozc7ACcLrEtDMZ1m1KrlN0SCx+0ECu/eYIjmbga9Qm4TERb52Gv/osZiz4HRUZb4zLumAAITukufQtnXL+026EG48jokwq9bTeD6SrsaYx/6czgC0EFEXGE+c26iPs0YdikeUSHoVg2QWlTwoBeABicJx/mvt+46HnaBu2yWbTrHAXeQo/g8jvNWW/01w2ue+gOT7anKnPqA3O2tc9tX1hOvmHAsFNMxQZyDMAyGwshy+fCvyzNwbvZmarMbDWwZgt3w8UgxWIccdyHaqfiGbO0v62RP000d207n78YexMpXTtsfC6vfvCjS4EgZX5BVg9UdWfHasz3dA1O48XZCohiCk/6slhSe5IqJjrEZo3bvFI+BXc6Jhmzto8CHcVLZr3nXkL0X4OWfL2SSfwjfW8uz+rA==,iv:VQYKe9GilKIRqDEVWKZtls1k+CfaEi8YhdXVKb9nh78=,tag:d4OYBzgyKA5KDkj9gz9CXg==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1s29qgfupgx7mqn9dk2rndp6xh63f5qan8vqa77ve603wd5m9a9rqkpq7re + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyTXRaUkNqVWpVY2NEdGo4 + QUNxbW41VWN3Mkx3NDRqbld0NlMyME5SRjJzCmEyZnVrWnRCcnoxbWE2bXFPQ1pk + ME81VTIxMlluTW0rYmlHS0tlK1NONk0KLS0tIEhZN1BIdnk4dHRkY3JyTmthZjR6 + aDFtMXJPbzhxYU1RaGVyUjYvc3Z2YUUKHh8Y5iuPB8D0VOxZvXQX61Uoae525ajZ + vc21qI0li9fBTmKNy0VYHDLvboP83jLNsAMd2To+xHmJimkhiivkew== + -----END AGE ENCRYPTED FILE----- + - recipient: age10h669gfc0yfdrfj4gtnsrpdfvrsh55tn0cj8kuk5t3x5e079dpgs30cuxw + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBNzNQMkorTkdISUJScUdr + SW9qUjJNU2hvbmhJRTQ3MG1sY1QxUWNVZ0YwCnpTS0FRKzZpbWpyOEpPMlFCQlhw + NmVVazNhZHBYdEFnMS9JT0Y2WUo1OTgKLS0tIEJ0Y3huYXJzY3poRkhTeXlOTXRv + MTFtYnh5Y0ZXMzdhV3ZqUjVLQjAzN0UKEFfu1TlRUHkyGDg2PLWQdm0VfXUiGZDZ + u5UsCR6bmvAMblGg4bXLmXhH61H97n6aJMS92j+G8uE4BDE1q/y4Tw== + -----END AGE ENCRYPTED FILE----- + - recipient: age1e52v0cr69sycuh59hksx9nk5327vgkemkxqwngd25gtztxhkpups87yd5g + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5Um1lQ0tVcGh0ZUQ2TGUv + OG1Qck9iZ0JodkVPNGUwNCt5ZzI2NGxhQ1VvCmF1SmpNL2o5WlQrdTRYSTV0WUY2 + Um5hVUp4ODFLUWx4ckl4WlMxUEhYWEkKLS0tIFBBRWlDWlEvR3g3alY5emNaRVI4 + VTVIMjJSaGVRY3hYbmhzUmFiaCtxVUEKmGFgyEVxF0s8UqrbV9vpVonzEqMdx/9z + yeAc9I9fdMiakISPvkh9Jkexc4duFS1noGTIAXPfCEriIupol2MVvw== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-10-04T13:22:15Z" + mac: ENC[AES256_GCM,data:8iW4SK8nAwaqfSr/FMebDZlF+8oUzu1YdK7sMoLCUEtYsuXCkjtHos9PAbayxmy5Mn3HipeJoqnkgPF9e0KDcvG2iwhIGTilheFx0qHvWz67cY5WdH0ykKVTbo1Kzcn3fhy/jzEzEYW1/hIPmrxZ82fA2jDMl9voSrVBgF6Rwg8=,iv:X7u4QwwXf54sK3bOtxQ6wgiswZF12zhNwhfsc0z2758=,tag:kS0SNx7YqUjWODoURDSpIg==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.9.0 diff --git a/shell.nix b/shell.nix @@ -0,0 +1,11 @@ +{ + pkgs ? import <nixpkgs> { }, +}: +pkgs.mkShell { + buildInputs = with pkgs; [ + curl + colmena + wireguard-tools + sops + ]; +} diff --git a/vpn.nix b/vpn.nix @@ -0,0 +1,69 @@ +{ pkgs, ... }: + +{ + networking.nat = { + enable = true; + externalInterface = "eth0"; + internalInterfaces = [ "wg0" ]; + }; + # Port 53 for DNS, port 51820 for WireGuard + networking.firewall = { + allowedTCPPorts = [ 53 ]; + allowedUDPPorts = [ + 53 + 51820 + ]; + }; + # boot.kernel.sysctl = { + # # "net.ipv4.ip_forward" = lib.mkOverride 98 true; + # "net.ipv4.conf.all.forwarding" = lib.mkOverride 98 true; + # "net.ipv4.conf.default.forwarding" = lib.mkOverride 98 true; + # }; + services.dnsmasq = { + enable = true; + settings = { + interface = "wg0"; + }; + }; + networking.wg-quick.interfaces = { + wg0 = { + address = [ "10.100.0.1/24" ]; + listenPort = 51820; + privateKeyFile = "/run/secrets/server_wireguard_key"; + + postUp = '' + ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -A FORWARD -o wg0 -i eth0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s 10.100.0.1/24 -j MASQUERADE + ''; + preDown = '' + ${pkgs.iptables}/bin/iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -D FORWARD -o wg0 -i eth0 -j ACCEPT + ${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -s 10.100.0.1/24 -j MASQUERADE + ''; + + peers = [ + { + # Thinkpad + publicKey = "3owbBawNk7IEI8sHmJjr+Wna7QH5WxYuRGjU+OP4i2M="; + allowedIPs = [ "10.100.0.2/32" ]; + } + { + # Pixel + publicKey = "pTuUtRphmXYKhwInaCIQY7nXRgftNzgKJxleACutrFc="; + allowedIPs = [ "10.100.0.3/32" ]; + } + { + # Desktop + publicKey = "F/UODKJ2SQaRUOb7my2LtvRaN4p/khfW8bstO+09/zo="; + allowedIPs = [ "10.100.0.4/32" ]; + } + { + # iPad + publicKey = "KwQo8pMz0JYHaEV3sbezmpD8jwgbXjaumIKeqWYaeiE="; + allowedIPs = [ "10.100.0.5/32" ]; + } + ]; + }; + }; +}