commit fc8038ad0065740073174f9f1facc6928522f0f1
parent 031131697a9c3737367436d0fcee822c043c8391
Author: Silas Brack <silasbrack@gmail.com>
Date: Thu, 10 Oct 2024 20:03:39 +0200
Move to flake
Diffstat:
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
+
+[](https://hex.pm/packages/my_app)
+[](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" ];
+ }
+ ];
+ };
+ };
+}