From 7e15690d54ff849596401b43d163df9353062850 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Sun, 23 Mar 2025 15:33:23 -0400 Subject: Rename `user` to `login` at the database. --- ...fd391b5812dc2d1d4a52ea3072b5dd52d71637b33d.json | 20 --- ...776b5d0e847cf52c68f24f5ea878b897944e70254c.json | 20 +++ ...d81d0295426d1148e786098b6d1a61a9c7d645c902.json | 50 +++++++ ...32d8eb2d9c2bb4de2fbbf0b2280973966ef02f72b1.json | 50 ------- ...b9c3126c22b432df8756ab007191f60702de270878.json | 20 +++ ...713f6354d6150088a052d1f261853f327a83e8dd75.json | 20 +++ ...9da95bc8271dfc8c6ec8ebf174ba8a57111b322291.json | 44 ------ ...af0c246e75ce95f6eb10f705c16b22345d31aefc61.json | 20 --- ...224bfc431f2e517755983f3510565e18ecb0e6e637.json | 38 ------ ...826c2d05f1b173691846e36370bccf166c08ce1955.json | 50 +++++++ ...213750fe202abfa2af5347010dee59d3b8075eb19e.json | 20 +++ ...895dff436d67edf6669a555fe20b178e9ba3039b0c.json | 44 ++++++ ...7ef7a26f6b14e1d30c3fb44ece8fb149c92317fa91.json | 26 ---- ...4db639b7a4a21210ebf0b7fba97cd016ff6ab4d769.json | 20 --- ...0717bf42e5d43b041acb198710e75417ac40991ec6.json | 50 ------- ...d84b6ac46f63d2804ef3528d833e6d17bec8864454.json | 44 ++++++ ...b041e5b5206628c3f0cd5230f6671bf8d8946d01f2.json | 44 ++++++ ...a977d13047cefdebee841d0df3c671a2104b9aef8f.json | 26 ++++ ...c233ed32638e853e3b8b8f8de26b53f90c98b6ce11.json | 20 --- ...d188879a394d36e66f90edb27d2665f081ff95087f.json | 44 ------ ...6379d16a29b4d0d1348e31599dc47522849f759066.json | 38 ++++++ ...932ace995f53efd6c7800a7a1b42daec41d081b3d2.json | 44 ------ ...5518d6364c8dbfdb45f205fecf0734fe272be493b0.json | 12 -- ...29f3a52be8d3134fc68e37c6074c69970604ae3844.json | 12 ++ migrations/20250323190045_rename_login_to_user.sql | 151 +++++++++++++++++++++ src/invite/repo.rs | 2 +- src/login/repo.rs | 10 +- src/setup/repo.rs | 2 +- src/token/repo/auth.rs | 4 +- src/token/repo/token.rs | 10 +- 30 files changed, 553 insertions(+), 402 deletions(-) delete mode 100644 .sqlx/query-0b1543ec93e02c48c5cbaafd391b5812dc2d1d4a52ea3072b5dd52d71637b33d.json create mode 100644 .sqlx/query-0be9484c8d277b08a925b7776b5d0e847cf52c68f24f5ea878b897944e70254c.json create mode 100644 .sqlx/query-0d4e10f80c1f6d605c70c9d81d0295426d1148e786098b6d1a61a9c7d645c902.json delete mode 100644 .sqlx/query-0f3bfb1ad8fad5213f733b32d8eb2d9c2bb4de2fbbf0b2280973966ef02f72b1.json create mode 100644 .sqlx/query-1a6654e50f9cbfe09a0a75b9c3126c22b432df8756ab007191f60702de270878.json create mode 100644 .sqlx/query-27c3ffb5c284cf2d1c7cee713f6354d6150088a052d1f261853f327a83e8dd75.json delete mode 100644 .sqlx/query-2c20c29d9adfed6201a6a69da95bc8271dfc8c6ec8ebf174ba8a57111b322291.json delete mode 100644 .sqlx/query-3c7cca4f823bd5e4cb0562af0c246e75ce95f6eb10f705c16b22345d31aefc61.json delete mode 100644 .sqlx/query-584aea21a5ceb0ce6e48bc224bfc431f2e517755983f3510565e18ecb0e6e637.json create mode 100644 .sqlx/query-5f9dfdff6e5067a02f9f01826c2d05f1b173691846e36370bccf166c08ce1955.json create mode 100644 .sqlx/query-613199c25e16a52d85761e213750fe202abfa2af5347010dee59d3b8075eb19e.json create mode 100644 .sqlx/query-6755a7bba5733f4a8090bc895dff436d67edf6669a555fe20b178e9ba3039b0c.json delete mode 100644 .sqlx/query-6d34d8232e7247155c697f7ef7a26f6b14e1d30c3fb44ece8fb149c92317fa91.json delete mode 100644 .sqlx/query-8b474c8ed7859f745888644db639b7a4a21210ebf0b7fba97cd016ff6ab4d769.json delete mode 100644 .sqlx/query-903e7ec4fafd5ce124a0e40717bf42e5d43b041acb198710e75417ac40991ec6.json create mode 100644 .sqlx/query-9e4e8544c86e36901b4543d84b6ac46f63d2804ef3528d833e6d17bec8864454.json create mode 100644 .sqlx/query-9eb1e7d793fe6a992dc937b041e5b5206628c3f0cd5230f6671bf8d8946d01f2.json create mode 100644 .sqlx/query-bb1be4fab0d7fa56ae5a8ea977d13047cefdebee841d0df3c671a2104b9aef8f.json delete mode 100644 .sqlx/query-c2b0ff7e2f27b6970a16fbc233ed32638e853e3b8b8f8de26b53f90c98b6ce11.json delete mode 100644 .sqlx/query-c31c02aa8c4e615c463835d188879a394d36e66f90edb27d2665f081ff95087f.json create mode 100644 .sqlx/query-c58e61c57373a7f19d98c46379d16a29b4d0d1348e31599dc47522849f759066.json delete mode 100644 .sqlx/query-cbf29fae3725bbb3d9e94d932ace995f53efd6c7800a7a1b42daec41d081b3d2.json delete mode 100644 .sqlx/query-e01508e57cc9cecc83640a5518d6364c8dbfdb45f205fecf0734fe272be493b0.json create mode 100644 .sqlx/query-f9abb172f96bff3a5fb4ad29f3a52be8d3134fc68e37c6074c69970604ae3844.json create mode 100644 migrations/20250323190045_rename_login_to_user.sql diff --git a/.sqlx/query-0b1543ec93e02c48c5cbaafd391b5812dc2d1d4a52ea3072b5dd52d71637b33d.json b/.sqlx/query-0b1543ec93e02c48c5cbaafd391b5812dc2d1d4a52ea3072b5dd52d71637b33d.json deleted file mode 100644 index 937b07e..0000000 --- a/.sqlx/query-0b1543ec93e02c48c5cbaafd391b5812dc2d1d4a52ea3072b5dd52d71637b33d.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n delete\n from token\n where login = $1\n returning id as \"id: Id\"\n ", - "describe": { - "columns": [ - { - "name": "id: Id", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "0b1543ec93e02c48c5cbaafd391b5812dc2d1d4a52ea3072b5dd52d71637b33d" -} diff --git a/.sqlx/query-0be9484c8d277b08a925b7776b5d0e847cf52c68f24f5ea878b897944e70254c.json b/.sqlx/query-0be9484c8d277b08a925b7776b5d0e847cf52c68f24f5ea878b897944e70254c.json new file mode 100644 index 0000000..182f996 --- /dev/null +++ b/.sqlx/query-0be9484c8d277b08a925b7776b5d0e847cf52c68f24f5ea878b897944e70254c.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n insert\n into token (id, secret, user, issued_at, last_used_at)\n values ($1, $2, $3, $4, $4)\n returning secret as \"secret!: Secret\"\n ", + "describe": { + "columns": [ + { + "name": "secret!: Secret", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 4 + }, + "nullable": [ + false + ] + }, + "hash": "0be9484c8d277b08a925b7776b5d0e847cf52c68f24f5ea878b897944e70254c" +} diff --git a/.sqlx/query-0d4e10f80c1f6d605c70c9d81d0295426d1148e786098b6d1a61a9c7d645c902.json b/.sqlx/query-0d4e10f80c1f6d605c70c9d81d0295426d1148e786098b6d1a61a9c7d645c902.json new file mode 100644 index 0000000..57e4a6f --- /dev/null +++ b/.sqlx/query-0d4e10f80c1f6d605c70c9d81d0295426d1148e786098b6d1a61a9c7d645c902.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "\n select\n id as \"id: login::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\",\n password_hash as \"password_hash: StoredHash\"\n from user\n where id = $1\n ", + "describe": { + "columns": [ + { + "name": "id: login::Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "display_name: String", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "canonical_name: String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_sequence: Sequence", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "created_at: DateTime", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "password_hash: StoredHash", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "0d4e10f80c1f6d605c70c9d81d0295426d1148e786098b6d1a61a9c7d645c902" +} diff --git a/.sqlx/query-0f3bfb1ad8fad5213f733b32d8eb2d9c2bb4de2fbbf0b2280973966ef02f72b1.json b/.sqlx/query-0f3bfb1ad8fad5213f733b32d8eb2d9c2bb4de2fbbf0b2280973966ef02f72b1.json deleted file mode 100644 index ffd81dc..0000000 --- a/.sqlx/query-0f3bfb1ad8fad5213f733b32d8eb2d9c2bb4de2fbbf0b2280973966ef02f72b1.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n id as \"id: login::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\",\n password_hash as \"password_hash: StoredHash\"\n from login\n where id = $1\n ", - "describe": { - "columns": [ - { - "name": "id: login::Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "display_name: String", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "canonical_name: String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "created_sequence: Sequence", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_at: DateTime", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "password_hash: StoredHash", - "ordinal": 5, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - false - ] - }, - "hash": "0f3bfb1ad8fad5213f733b32d8eb2d9c2bb4de2fbbf0b2280973966ef02f72b1" -} diff --git a/.sqlx/query-1a6654e50f9cbfe09a0a75b9c3126c22b432df8756ab007191f60702de270878.json b/.sqlx/query-1a6654e50f9cbfe09a0a75b9c3126c22b432df8756ab007191f60702de270878.json new file mode 100644 index 0000000..2f114b8 --- /dev/null +++ b/.sqlx/query-1a6654e50f9cbfe09a0a75b9c3126c22b432df8756ab007191f60702de270878.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n select count(*) > 0 as \"completed: bool\"\n from user\n ", + "describe": { + "columns": [ + { + "name": "completed: bool", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "1a6654e50f9cbfe09a0a75b9c3126c22b432df8756ab007191f60702de270878" +} diff --git a/.sqlx/query-27c3ffb5c284cf2d1c7cee713f6354d6150088a052d1f261853f327a83e8dd75.json b/.sqlx/query-27c3ffb5c284cf2d1c7cee713f6354d6150088a052d1f261853f327a83e8dd75.json new file mode 100644 index 0000000..c02667e --- /dev/null +++ b/.sqlx/query-27c3ffb5c284cf2d1c7cee713f6354d6150088a052d1f261853f327a83e8dd75.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n update user\n set password_hash = $1\n where id = $2\n returning id as \"id: Id\"\n ", + "describe": { + "columns": [ + { + "name": "id: Id", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "27c3ffb5c284cf2d1c7cee713f6354d6150088a052d1f261853f327a83e8dd75" +} diff --git a/.sqlx/query-2c20c29d9adfed6201a6a69da95bc8271dfc8c6ec8ebf174ba8a57111b322291.json b/.sqlx/query-2c20c29d9adfed6201a6a69da95bc8271dfc8c6ec8ebf174ba8a57111b322291.json deleted file mode 100644 index ae546ad..0000000 --- a/.sqlx/query-2c20c29d9adfed6201a6a69da95bc8271dfc8c6ec8ebf174ba8a57111b322291.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n id as \"id: Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from login\n where login.created_sequence > $1\n ", - "describe": { - "columns": [ - { - "name": "id: Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "display_name: String", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "canonical_name: String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "created_sequence: Sequence", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_at: DateTime", - "ordinal": 4, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false - ] - }, - "hash": "2c20c29d9adfed6201a6a69da95bc8271dfc8c6ec8ebf174ba8a57111b322291" -} diff --git a/.sqlx/query-3c7cca4f823bd5e4cb0562af0c246e75ce95f6eb10f705c16b22345d31aefc61.json b/.sqlx/query-3c7cca4f823bd5e4cb0562af0c246e75ce95f6eb10f705c16b22345d31aefc61.json deleted file mode 100644 index 6aab5fc..0000000 --- a/.sqlx/query-3c7cca4f823bd5e4cb0562af0c246e75ce95f6eb10f705c16b22345d31aefc61.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select count(*) > 0 as \"completed: bool\"\n from login\n ", - "describe": { - "columns": [ - { - "name": "completed: bool", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "3c7cca4f823bd5e4cb0562af0c246e75ce95f6eb10f705c16b22345d31aefc61" -} diff --git a/.sqlx/query-584aea21a5ceb0ce6e48bc224bfc431f2e517755983f3510565e18ecb0e6e637.json b/.sqlx/query-584aea21a5ceb0ce6e48bc224bfc431f2e517755983f3510565e18ecb0e6e637.json deleted file mode 100644 index f443d9a..0000000 --- a/.sqlx/query-584aea21a5ceb0ce6e48bc224bfc431f2e517755983f3510565e18ecb0e6e637.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n invite.id as \"invite_id: Id\",\n issuer.id as \"issuer_id: login::Id\",\n issuer.display_name as \"issuer_name: nfc::String\",\n invite.issued_at as \"invite_issued_at: DateTime\"\n from invite\n join login as issuer on (invite.issuer = issuer.id)\n where invite.id = $1\n ", - "describe": { - "columns": [ - { - "name": "invite_id: Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "issuer_id: login::Id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "issuer_name: nfc::String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "invite_issued_at: DateTime", - "ordinal": 3, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "584aea21a5ceb0ce6e48bc224bfc431f2e517755983f3510565e18ecb0e6e637" -} diff --git a/.sqlx/query-5f9dfdff6e5067a02f9f01826c2d05f1b173691846e36370bccf166c08ce1955.json b/.sqlx/query-5f9dfdff6e5067a02f9f01826c2d05f1b173691846e36370bccf166c08ce1955.json new file mode 100644 index 0000000..215c0b5 --- /dev/null +++ b/.sqlx/query-5f9dfdff6e5067a02f9f01826c2d05f1b173691846e36370bccf166c08ce1955.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "\n select\n id as \"id: login::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\",\n password_hash as \"password_hash: StoredHash\"\n from user\n where canonical_name = $1\n ", + "describe": { + "columns": [ + { + "name": "id: login::Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "display_name: String", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "canonical_name: String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_sequence: Sequence", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "created_at: DateTime", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "password_hash: StoredHash", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "5f9dfdff6e5067a02f9f01826c2d05f1b173691846e36370bccf166c08ce1955" +} diff --git a/.sqlx/query-613199c25e16a52d85761e213750fe202abfa2af5347010dee59d3b8075eb19e.json b/.sqlx/query-613199c25e16a52d85761e213750fe202abfa2af5347010dee59d3b8075eb19e.json new file mode 100644 index 0000000..1657efa --- /dev/null +++ b/.sqlx/query-613199c25e16a52d85761e213750fe202abfa2af5347010dee59d3b8075eb19e.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n delete\n from token\n where user = $1\n returning id as \"id: Id\"\n ", + "describe": { + "columns": [ + { + "name": "id: Id", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "613199c25e16a52d85761e213750fe202abfa2af5347010dee59d3b8075eb19e" +} diff --git a/.sqlx/query-6755a7bba5733f4a8090bc895dff436d67edf6669a555fe20b178e9ba3039b0c.json b/.sqlx/query-6755a7bba5733f4a8090bc895dff436d67edf6669a555fe20b178e9ba3039b0c.json new file mode 100644 index 0000000..94bc39f --- /dev/null +++ b/.sqlx/query-6755a7bba5733f4a8090bc895dff436d67edf6669a555fe20b178e9ba3039b0c.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n select\n id as \"id: login::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from user\n where id = $1\n ", + "describe": { + "columns": [ + { + "name": "id: login::Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "display_name: String", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "canonical_name: String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_sequence: Sequence", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "created_at: DateTime", + "ordinal": 4, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "6755a7bba5733f4a8090bc895dff436d67edf6669a555fe20b178e9ba3039b0c" +} diff --git a/.sqlx/query-6d34d8232e7247155c697f7ef7a26f6b14e1d30c3fb44ece8fb149c92317fa91.json b/.sqlx/query-6d34d8232e7247155c697f7ef7a26f6b14e1d30c3fb44ece8fb149c92317fa91.json deleted file mode 100644 index 93a4093..0000000 --- a/.sqlx/query-6d34d8232e7247155c697f7ef7a26f6b14e1d30c3fb44ece8fb149c92317fa91.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n update token\n set last_used_at = $1\n where secret = $2\n returning\n id as \"token: Id\",\n login as \"login: login::Id\"\n ", - "describe": { - "columns": [ - { - "name": "token: Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "login: login::Id", - "ordinal": 1, - "type_info": "Text" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false - ] - }, - "hash": "6d34d8232e7247155c697f7ef7a26f6b14e1d30c3fb44ece8fb149c92317fa91" -} diff --git a/.sqlx/query-8b474c8ed7859f745888644db639b7a4a21210ebf0b7fba97cd016ff6ab4d769.json b/.sqlx/query-8b474c8ed7859f745888644db639b7a4a21210ebf0b7fba97cd016ff6ab4d769.json deleted file mode 100644 index b433e4c..0000000 --- a/.sqlx/query-8b474c8ed7859f745888644db639b7a4a21210ebf0b7fba97cd016ff6ab4d769.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n insert\n into token (id, secret, login, issued_at, last_used_at)\n values ($1, $2, $3, $4, $4)\n returning secret as \"secret!: Secret\"\n ", - "describe": { - "columns": [ - { - "name": "secret!: Secret", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 4 - }, - "nullable": [ - false - ] - }, - "hash": "8b474c8ed7859f745888644db639b7a4a21210ebf0b7fba97cd016ff6ab4d769" -} diff --git a/.sqlx/query-903e7ec4fafd5ce124a0e40717bf42e5d43b041acb198710e75417ac40991ec6.json b/.sqlx/query-903e7ec4fafd5ce124a0e40717bf42e5d43b041acb198710e75417ac40991ec6.json deleted file mode 100644 index cf1afec..0000000 --- a/.sqlx/query-903e7ec4fafd5ce124a0e40717bf42e5d43b041acb198710e75417ac40991ec6.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n id as \"id: login::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\",\n password_hash as \"password_hash: StoredHash\"\n from login\n where canonical_name = $1\n ", - "describe": { - "columns": [ - { - "name": "id: login::Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "display_name: String", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "canonical_name: String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "created_sequence: Sequence", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_at: DateTime", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "password_hash: StoredHash", - "ordinal": 5, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - false - ] - }, - "hash": "903e7ec4fafd5ce124a0e40717bf42e5d43b041acb198710e75417ac40991ec6" -} diff --git a/.sqlx/query-9e4e8544c86e36901b4543d84b6ac46f63d2804ef3528d833e6d17bec8864454.json b/.sqlx/query-9e4e8544c86e36901b4543d84b6ac46f63d2804ef3528d833e6d17bec8864454.json new file mode 100644 index 0000000..beacb24 --- /dev/null +++ b/.sqlx/query-9e4e8544c86e36901b4543d84b6ac46f63d2804ef3528d833e6d17bec8864454.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n select\n id as \"id: Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from user\n where created_sequence > $1\n ", + "describe": { + "columns": [ + { + "name": "id: Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "display_name: String", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "canonical_name: String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_sequence: Sequence", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "created_at: DateTime", + "ordinal": 4, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "9e4e8544c86e36901b4543d84b6ac46f63d2804ef3528d833e6d17bec8864454" +} diff --git a/.sqlx/query-9eb1e7d793fe6a992dc937b041e5b5206628c3f0cd5230f6671bf8d8946d01f2.json b/.sqlx/query-9eb1e7d793fe6a992dc937b041e5b5206628c3f0cd5230f6671bf8d8946d01f2.json new file mode 100644 index 0000000..45177f3 --- /dev/null +++ b/.sqlx/query-9eb1e7d793fe6a992dc937b041e5b5206628c3f0cd5230f6671bf8d8946d01f2.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n select\n id as \"id: Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from user\n where created_sequence <= $1\n order by canonical_name\n ", + "describe": { + "columns": [ + { + "name": "id: Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "display_name: String", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "canonical_name: String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_sequence: Sequence", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "created_at: DateTime", + "ordinal": 4, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "9eb1e7d793fe6a992dc937b041e5b5206628c3f0cd5230f6671bf8d8946d01f2" +} diff --git a/.sqlx/query-bb1be4fab0d7fa56ae5a8ea977d13047cefdebee841d0df3c671a2104b9aef8f.json b/.sqlx/query-bb1be4fab0d7fa56ae5a8ea977d13047cefdebee841d0df3c671a2104b9aef8f.json new file mode 100644 index 0000000..a32a555 --- /dev/null +++ b/.sqlx/query-bb1be4fab0d7fa56ae5a8ea977d13047cefdebee841d0df3c671a2104b9aef8f.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n update token\n set last_used_at = $1\n where secret = $2\n returning\n id as \"token: Id\",\n user as \"user: login::Id\"\n ", + "describe": { + "columns": [ + { + "name": "token: Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "user: login::Id", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false + ] + }, + "hash": "bb1be4fab0d7fa56ae5a8ea977d13047cefdebee841d0df3c671a2104b9aef8f" +} diff --git a/.sqlx/query-c2b0ff7e2f27b6970a16fbc233ed32638e853e3b8b8f8de26b53f90c98b6ce11.json b/.sqlx/query-c2b0ff7e2f27b6970a16fbc233ed32638e853e3b8b8f8de26b53f90c98b6ce11.json deleted file mode 100644 index 4c99c42..0000000 --- a/.sqlx/query-c2b0ff7e2f27b6970a16fbc233ed32638e853e3b8b8f8de26b53f90c98b6ce11.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n update login\n set password_hash = $1\n where id = $2\n returning id as \"id: Id\"\n ", - "describe": { - "columns": [ - { - "name": "id: Id", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false - ] - }, - "hash": "c2b0ff7e2f27b6970a16fbc233ed32638e853e3b8b8f8de26b53f90c98b6ce11" -} diff --git a/.sqlx/query-c31c02aa8c4e615c463835d188879a394d36e66f90edb27d2665f081ff95087f.json b/.sqlx/query-c31c02aa8c4e615c463835d188879a394d36e66f90edb27d2665f081ff95087f.json deleted file mode 100644 index aa20875..0000000 --- a/.sqlx/query-c31c02aa8c4e615c463835d188879a394d36e66f90edb27d2665f081ff95087f.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n id as \"id: login::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from login\n where id = $1\n ", - "describe": { - "columns": [ - { - "name": "id: login::Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "display_name: String", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "canonical_name: String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "created_sequence: Sequence", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_at: DateTime", - "ordinal": 4, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false - ] - }, - "hash": "c31c02aa8c4e615c463835d188879a394d36e66f90edb27d2665f081ff95087f" -} diff --git a/.sqlx/query-c58e61c57373a7f19d98c46379d16a29b4d0d1348e31599dc47522849f759066.json b/.sqlx/query-c58e61c57373a7f19d98c46379d16a29b4d0d1348e31599dc47522849f759066.json new file mode 100644 index 0000000..18d720c --- /dev/null +++ b/.sqlx/query-c58e61c57373a7f19d98c46379d16a29b4d0d1348e31599dc47522849f759066.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n select\n invite.id as \"invite_id: Id\",\n issuer.id as \"issuer_id: login::Id\",\n issuer.display_name as \"issuer_name: nfc::String\",\n invite.issued_at as \"invite_issued_at: DateTime\"\n from invite\n join user as issuer on (invite.issuer = issuer.id)\n where invite.id = $1\n ", + "describe": { + "columns": [ + { + "name": "invite_id: Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "issuer_id: login::Id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "issuer_name: nfc::String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "invite_issued_at: DateTime", + "ordinal": 3, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "c58e61c57373a7f19d98c46379d16a29b4d0d1348e31599dc47522849f759066" +} diff --git a/.sqlx/query-cbf29fae3725bbb3d9e94d932ace995f53efd6c7800a7a1b42daec41d081b3d2.json b/.sqlx/query-cbf29fae3725bbb3d9e94d932ace995f53efd6c7800a7a1b42daec41d081b3d2.json deleted file mode 100644 index 9c3c10e..0000000 --- a/.sqlx/query-cbf29fae3725bbb3d9e94d932ace995f53efd6c7800a7a1b42daec41d081b3d2.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n id as \"id: Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from login\n where created_sequence <= $1\n order by canonical_name\n ", - "describe": { - "columns": [ - { - "name": "id: Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "display_name: String", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "canonical_name: String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "created_sequence: Sequence", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_at: DateTime", - "ordinal": 4, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false - ] - }, - "hash": "cbf29fae3725bbb3d9e94d932ace995f53efd6c7800a7a1b42daec41d081b3d2" -} diff --git a/.sqlx/query-e01508e57cc9cecc83640a5518d6364c8dbfdb45f205fecf0734fe272be493b0.json b/.sqlx/query-e01508e57cc9cecc83640a5518d6364c8dbfdb45f205fecf0734fe272be493b0.json deleted file mode 100644 index 4efac0c..0000000 --- a/.sqlx/query-e01508e57cc9cecc83640a5518d6364c8dbfdb45f205fecf0734fe272be493b0.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n insert\n into login (id, display_name, canonical_name, password_hash, created_sequence, created_at)\n values ($1, $2, $3, $4, $5, $6)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "e01508e57cc9cecc83640a5518d6364c8dbfdb45f205fecf0734fe272be493b0" -} diff --git a/.sqlx/query-f9abb172f96bff3a5fb4ad29f3a52be8d3134fc68e37c6074c69970604ae3844.json b/.sqlx/query-f9abb172f96bff3a5fb4ad29f3a52be8d3134fc68e37c6074c69970604ae3844.json new file mode 100644 index 0000000..9d1bc77 --- /dev/null +++ b/.sqlx/query-f9abb172f96bff3a5fb4ad29f3a52be8d3134fc68e37c6074c69970604ae3844.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n insert\n into user (id, display_name, canonical_name, password_hash, created_sequence, created_at)\n values ($1, $2, $3, $4, $5, $6)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "f9abb172f96bff3a5fb4ad29f3a52be8d3134fc68e37c6074c69970604ae3844" +} diff --git a/migrations/20250323190045_rename_login_to_user.sql b/migrations/20250323190045_rename_login_to_user.sql new file mode 100644 index 0000000..7b5861d --- /dev/null +++ b/migrations/20250323190045_rename_login_to_user.sql @@ -0,0 +1,151 @@ +-- message +-- message_deleted + +-- Set up the new `user` table, and copy existing logins into it +create table user +( + id text + not null + primary key, + display_name text + not null, + canonical_name text + not null + unique, + password_hash text + not null, + created_sequence bigint + unique + not null, + created_at text + not null +); + +insert into user (id, display_name, canonical_name, password_hash, created_sequence, created_at) +select id, display_name, canonical_name, password_hash, created_sequence, created_at +from login; + +-- Carry the rename through `token`, preserving data… +alter table token + rename to old_token; + +create table token +( + id text + not null + primary key, + secret text + not null + unique, + user text + not null, + issued_at text + not null, + last_used_at text + not null, + foreign key (user) + references user (id) +); + +insert into token (id, secret, user, issued_at, last_used_at) +select id, secret, login, issued_at, last_used_at +from old_token; + +-- Carry the rename through `invite`, preserving data… +alter table invite + rename to old_invite; + +create table invite +( + id text + primary key + not null, + issuer text + not null + references user (id), + issued_at text + not null +); + +insert into invite (id, issuer, issued_at) +select id, issuer, issued_at +from old_invite; + +-- Carry the rename through `message`, preserving data… +alter table message + rename to old_message; + +create table message +( + id text + not null + primary key, + channel text + not null + references channel (id), + sender text + not null + references user (id), + sent_sequence bigint + unique + not null, + sent_at text + not null, + body text + null, + last_sequence bigint + not null +); + +insert into message (id, channel, sender, sent_sequence, sent_at, body, last_sequence) +select id, channel, sender, sent_sequence, sent_at, body, last_sequence +from old_message; + +-- Recreating `message` entails recreating `message_deleted` +alter table message_deleted + rename to old_message_deleted; + +create table message_deleted +( + id text + not null + primary key + references message (id), + deleted_sequence bigint + unique + not null, + deleted_at text + not null +); + +insert into message_deleted (id, deleted_sequence, deleted_at) +select id, deleted_sequence, deleted_at +from old_message_deleted; + +-- Delete old tables (which will take old indices with them) +drop table old_message_deleted; +drop table old_message; +drop table old_invite; +drop table old_token; +drop table login; + +-- Recreate indices +create index message_deleted_deleted_at + on message_deleted (deleted_at); + +create index message_sent_at + on message (sent_at); +create index message_channel + on message (channel); +create index message_last_sequence + on message (last_sequence); + +create index invite_issued_at + on invite (issued_at); + +create index token_issued_at + on token (issued_at); +create index token_last_used_at + on token (last_used_at); +create index token_user + on token (user); diff --git a/src/invite/repo.rs b/src/invite/repo.rs index c1dc701..c716ed9 100644 --- a/src/invite/repo.rs +++ b/src/invite/repo.rs @@ -74,7 +74,7 @@ impl Invites<'_> { issuer.display_name as "issuer_name: nfc::String", invite.issued_at as "invite_issued_at: DateTime" from invite - join login as issuer on (invite.issuer = issuer.id) + join user as issuer on (invite.issuer = issuer.id) where invite.id = $1 "#, invite, diff --git a/src/login/repo.rs b/src/login/repo.rs index 03f2c17..128d6b8 100644 --- a/src/login/repo.rs +++ b/src/login/repo.rs @@ -34,7 +34,7 @@ impl Logins<'_> { sqlx::query!( r#" insert - into login (id, display_name, canonical_name, password_hash, created_sequence, created_at) + into user (id, display_name, canonical_name, password_hash, created_sequence, created_at) values ($1, $2, $3, $4, $5, $6) "#, id, @@ -67,7 +67,7 @@ impl Logins<'_> { sqlx::query_scalar!( r#" - update login + update user set password_hash = $1 where id = $2 returning id as "id: Id" @@ -90,7 +90,7 @@ impl Logins<'_> { canonical_name as "canonical_name: String", created_sequence as "created_sequence: Sequence", created_at as "created_at: DateTime" - from login + from user where created_sequence <= $1 order by canonical_name "#, @@ -122,8 +122,8 @@ impl Logins<'_> { canonical_name as "canonical_name: String", created_sequence as "created_sequence: Sequence", created_at as "created_at: DateTime" - from login - where login.created_sequence > $1 + from user + where created_sequence > $1 "#, resume_at, ) diff --git a/src/setup/repo.rs b/src/setup/repo.rs index ac01496..c4f5fd8 100644 --- a/src/setup/repo.rs +++ b/src/setup/repo.rs @@ -17,7 +17,7 @@ impl Setup<'_> { let completed = sqlx::query_scalar!( r#" select count(*) > 0 as "completed: bool" - from login + from user "#, ) .fetch_one(&mut *self.0) diff --git a/src/token/repo/auth.rs b/src/token/repo/auth.rs index 0deed10..8900704 100644 --- a/src/token/repo/auth.rs +++ b/src/token/repo/auth.rs @@ -32,7 +32,7 @@ impl Auth<'_> { created_sequence as "created_sequence: Sequence", created_at as "created_at: DateTime", password_hash as "password_hash: StoredHash" - from login + from user where canonical_name = $1 "#, name, @@ -61,7 +61,7 @@ impl Auth<'_> { created_sequence as "created_sequence: Sequence", created_at as "created_at: DateTime", password_hash as "password_hash: StoredHash" - from login + from user where id = $1 "#, login.id, diff --git a/src/token/repo/token.rs b/src/token/repo/token.rs index ff42fad..3428030 100644 --- a/src/token/repo/token.rs +++ b/src/token/repo/token.rs @@ -37,7 +37,7 @@ impl Tokens<'_> { let secret = sqlx::query_scalar!( r#" insert - into token (id, secret, login, issued_at, last_used_at) + into token (id, secret, user, issued_at, last_used_at) values ($1, $2, $3, $4, $4) returning secret as "secret!: Secret" "#, @@ -91,7 +91,7 @@ impl Tokens<'_> { r#" delete from token - where login = $1 + where user = $1 returning id as "id: Id" "#, login, @@ -139,12 +139,12 @@ impl Tokens<'_> { where secret = $2 returning id as "token: Id", - login as "login: login::Id" + user as "user: login::Id" "#, used_at, secret, ) - .map(|row| (row.token, row.login)) + .map(|row| (row.token, row.user)) .fetch_one(&mut *self.0) .await?; @@ -156,7 +156,7 @@ impl Tokens<'_> { canonical_name as "canonical_name: String", created_sequence as "created_sequence: Sequence", created_at as "created_at: DateTime" - from login + from user where id = $1 "#, login, -- cgit v1.2.3 From 2420f1e75d54a5f209b0267715f078a369d81eb1 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Sun, 23 Mar 2025 15:58:33 -0400 Subject: Rename the `login` module to `user`. --- ...d81d0295426d1148e786098b6d1a61a9c7d645c902.json | 50 ------- ...f6ce56d372edeb35f92769a0181a71d68a68780649.json | 62 +++++++++ ...ab85129c951fb199dadb66f5c980ec30405b74a277.json | 32 +++++ ...e3d4f25cff1556aec7083bc484172c58cbd655a316.json | 32 ----- ...e6856eff74a544f0a1c3692766e48a3182df5ada98.json | 62 --------- ...8b9086c4f582bd2b3dc99a6a8e5f4853e0d4c43c50.json | 62 +++++++++ ...8d6d9da73e88a6cab35ae511674d25aa78bac9bc04.json | 62 +++++++++ ...9472f1138395b50cdfc9a28e9791e5484890f0201b.json | 62 --------- ...26a3c6adfcfa20b687ac886efe6d41d5abfd7bc183.json | 38 +++++ ...9075e71bd7e30dc93d32e1f273c878f18f2984860f.json | 62 --------- ...25c2ae66d6abb70f7bd2413cfbe23b71d1ce6090cd.json | 50 +++++++ ...826c2d05f1b173691846e36370bccf166c08ce1955.json | 50 ------- ...895dff436d67edf6669a555fe20b178e9ba3039b0c.json | 44 ------ ...8de3272e44b1c15ed75977ce24f25fd62042fb20fb.json | 26 ++++ ...7cf99b01395f30788ab60232f239ce85125e425b79.json | 32 +++++ ...41e4322f8026f9e2515b6bacaed81f6248c52a198a.json | 50 ------- ...0e83b4ef41bad03b57937a27f98aebbef5268ef5f5.json | 62 --------- ...66b2ca84cbe31458ff73a94648c30b62f3130a5a8b.json | 50 +++++++ ...af911f7488a708d5a50920bb042c0229c314ee3281.json | 44 ++++++ ...7cf0360789d46d94d9cd98887a3d9660d9b753d416.json | 50 +++++++ ...cd1c543db4a3f02c211462704f7810fdbed924ac30.json | 62 +++++++++ ...a977d13047cefdebee841d0df3c671a2104b9aef8f.json | 26 ---- ...6379d16a29b4d0d1348e31599dc47522849f759066.json | 38 ----- ...80b04a506cd63a85741923f841b7c36c46b70a538f.json | 32 ----- ...0f3f61ec2e20795692db7019d962378c740ae69599.json | 62 +++++++++ ...84a27bdaefca99706a0c73c17f19cc537f3f669882.json | 62 --------- src/app.rs | 6 +- src/boot/app.rs | 10 +- src/boot/mod.rs | 4 +- src/boot/routes/get.rs | 6 +- src/boot/routes/test.rs | 2 +- src/channel/routes/channel/post.rs | 2 +- src/channel/routes/channel/test/post.rs | 2 +- src/cli.rs | 6 +- src/event/app.rs | 12 +- src/event/mod.rs | 8 +- src/event/routes/test/invite.rs | 4 +- src/event/routes/test/setup.rs | 2 +- src/event/routes/test/token.rs | 2 +- src/invite/app.rs | 16 +-- src/invite/mod.rs | 4 +- src/invite/repo.rs | 10 +- src/invite/routes/invite/post.rs | 4 +- src/invite/routes/invite/test/post.rs | 4 +- src/invite/routes/post.rs | 2 +- src/invite/routes/test.rs | 2 +- src/lib.rs | 2 +- src/login/app.rs | 56 -------- src/login/create.rs | 95 ------------- src/login/event.rs | 36 ----- src/login/history.rs | 52 ------- src/login/id.rs | 24 ---- src/login/mod.rs | 15 -- src/login/password.rs | 65 --------- src/login/repo.rs | 153 --------------------- src/login/routes/login/mod.rs | 4 - src/login/routes/login/post.rs | 52 ------- src/login/routes/login/test.rs | 128 ----------------- src/login/routes/logout/mod.rs | 4 - src/login/routes/logout/post.rs | 47 ------- src/login/routes/logout/test.rs | 79 ----------- src/login/routes/mod.rs | 14 -- src/login/routes/password/mod.rs | 4 - src/login/routes/password/post.rs | 54 -------- src/login/routes/password/test.rs | 68 --------- src/login/snapshot.rs | 51 ------- src/login/validate.rs | 23 ---- src/message/app.rs | 10 +- src/message/repo.rs | 16 +-- src/message/routes/message/mod.rs | 2 +- src/message/routes/message/test.rs | 4 +- src/message/snapshot.rs | 4 +- src/setup/app.rs | 14 +- src/setup/routes/post.rs | 4 +- src/test/fixtures/cookie.rs | 2 +- src/test/fixtures/event.rs | 4 +- src/test/fixtures/identity.rs | 6 +- src/test/fixtures/invite.rs | 4 +- src/test/fixtures/login.rs | 14 +- src/test/fixtures/message.rs | 4 +- src/token/app.rs | 14 +- src/token/extract/identity.rs | 6 +- src/token/repo/auth.rs | 12 +- src/token/repo/token.rs | 10 +- src/user/app.rs | 56 ++++++++ src/user/create.rs | 95 +++++++++++++ src/user/event.rs | 36 +++++ src/user/history.rs | 52 +++++++ src/user/id.rs | 24 ++++ src/user/mod.rs | 15 ++ src/user/password.rs | 65 +++++++++ src/user/repo.rs | 153 +++++++++++++++++++++ src/user/routes/login/mod.rs | 4 + src/user/routes/login/post.rs | 52 +++++++ src/user/routes/login/test.rs | 128 +++++++++++++++++ src/user/routes/logout/mod.rs | 4 + src/user/routes/logout/post.rs | 47 +++++++ src/user/routes/logout/test.rs | 79 +++++++++++ src/user/routes/mod.rs | 14 ++ src/user/routes/password/mod.rs | 4 + src/user/routes/password/post.rs | 54 ++++++++ src/user/routes/password/test.rs | 68 +++++++++ src/user/snapshot.rs | 52 +++++++ src/user/validate.rs | 23 ++++ 104 files changed, 1777 insertions(+), 1776 deletions(-) delete mode 100644 .sqlx/query-0d4e10f80c1f6d605c70c9d81d0295426d1148e786098b6d1a61a9c7d645c902.json create mode 100644 .sqlx/query-17f6f507d9343a734e1098f6ce56d372edeb35f92769a0181a71d68a68780649.json create mode 100644 .sqlx/query-18aada5bab0b6c438b6b97ab85129c951fb199dadb66f5c980ec30405b74a277.json delete mode 100644 .sqlx/query-1946af14f5d3da9af51fc0e3d4f25cff1556aec7083bc484172c58cbd655a316.json delete mode 100644 .sqlx/query-2e0aa3126267465ee1ae01e6856eff74a544f0a1c3692766e48a3182df5ada98.json create mode 100644 .sqlx/query-3d0354407ac4dcd7b012328b9086c4f582bd2b3dc99a6a8e5f4853e0d4c43c50.json create mode 100644 .sqlx/query-3ff8a089ca1c57111e8c0e8d6d9da73e88a6cab35ae511674d25aa78bac9bc04.json delete mode 100644 .sqlx/query-4623f989492e9eae6788ee9472f1138395b50cdfc9a28e9791e5484890f0201b.json create mode 100644 .sqlx/query-53193435a6eeb72266d0e526a3c6adfcfa20b687ac886efe6d41d5abfd7bc183.json delete mode 100644 .sqlx/query-53b1f14d450a99f486bfd79075e71bd7e30dc93d32e1f273c878f18f2984860f.json create mode 100644 .sqlx/query-579bf0557d3e141dfd411c25c2ae66d6abb70f7bd2413cfbe23b71d1ce6090cd.json delete mode 100644 .sqlx/query-5f9dfdff6e5067a02f9f01826c2d05f1b173691846e36370bccf166c08ce1955.json delete mode 100644 .sqlx/query-6755a7bba5733f4a8090bc895dff436d67edf6669a555fe20b178e9ba3039b0c.json create mode 100644 .sqlx/query-684b4bef3e553b8ccfa20d8de3272e44b1c15ed75977ce24f25fd62042fb20fb.json create mode 100644 .sqlx/query-713ccbb31289eebd93840a7cf99b01395f30788ab60232f239ce85125e425b79.json delete mode 100644 .sqlx/query-72441293731853e9f0cc1141e4322f8026f9e2515b6bacaed81f6248c52a198a.json delete mode 100644 .sqlx/query-78d24fa907f3dcc0c129880e83b4ef41bad03b57937a27f98aebbef5268ef5f5.json create mode 100644 .sqlx/query-79495da0101ad12e7e983666b2ca84cbe31458ff73a94648c30b62f3130a5a8b.json create mode 100644 .sqlx/query-9cf211b1f37708fc8f19b3af911f7488a708d5a50920bb042c0229c314ee3281.json create mode 100644 .sqlx/query-afc6db503a3c49c18a9cb07cf0360789d46d94d9cd98887a3d9660d9b753d416.json create mode 100644 .sqlx/query-b18432e78891ffca0d7f3fcd1c543db4a3f02c211462704f7810fdbed924ac30.json delete mode 100644 .sqlx/query-bb1be4fab0d7fa56ae5a8ea977d13047cefdebee841d0df3c671a2104b9aef8f.json delete mode 100644 .sqlx/query-c58e61c57373a7f19d98c46379d16a29b4d0d1348e31599dc47522849f759066.json delete mode 100644 .sqlx/query-d49d4ab5f1bf4c78fa619680b04a506cd63a85741923f841b7c36c46b70a538f.json create mode 100644 .sqlx/query-fb7114754c6dc8ffe623ae0f3f61ec2e20795692db7019d962378c740ae69599.json delete mode 100644 .sqlx/query-ff61ff22108f1e98bbfc9a84a27bdaefca99706a0c73c17f19cc537f3f669882.json delete mode 100644 src/login/app.rs delete mode 100644 src/login/create.rs delete mode 100644 src/login/event.rs delete mode 100644 src/login/history.rs delete mode 100644 src/login/id.rs delete mode 100644 src/login/mod.rs delete mode 100644 src/login/password.rs delete mode 100644 src/login/repo.rs delete mode 100644 src/login/routes/login/mod.rs delete mode 100644 src/login/routes/login/post.rs delete mode 100644 src/login/routes/login/test.rs delete mode 100644 src/login/routes/logout/mod.rs delete mode 100644 src/login/routes/logout/post.rs delete mode 100644 src/login/routes/logout/test.rs delete mode 100644 src/login/routes/mod.rs delete mode 100644 src/login/routes/password/mod.rs delete mode 100644 src/login/routes/password/post.rs delete mode 100644 src/login/routes/password/test.rs delete mode 100644 src/login/snapshot.rs delete mode 100644 src/login/validate.rs create mode 100644 src/user/app.rs create mode 100644 src/user/create.rs create mode 100644 src/user/event.rs create mode 100644 src/user/history.rs create mode 100644 src/user/id.rs create mode 100644 src/user/mod.rs create mode 100644 src/user/password.rs create mode 100644 src/user/repo.rs create mode 100644 src/user/routes/login/mod.rs create mode 100644 src/user/routes/login/post.rs create mode 100644 src/user/routes/login/test.rs create mode 100644 src/user/routes/logout/mod.rs create mode 100644 src/user/routes/logout/post.rs create mode 100644 src/user/routes/logout/test.rs create mode 100644 src/user/routes/mod.rs create mode 100644 src/user/routes/password/mod.rs create mode 100644 src/user/routes/password/post.rs create mode 100644 src/user/routes/password/test.rs create mode 100644 src/user/snapshot.rs create mode 100644 src/user/validate.rs diff --git a/.sqlx/query-0d4e10f80c1f6d605c70c9d81d0295426d1148e786098b6d1a61a9c7d645c902.json b/.sqlx/query-0d4e10f80c1f6d605c70c9d81d0295426d1148e786098b6d1a61a9c7d645c902.json deleted file mode 100644 index 57e4a6f..0000000 --- a/.sqlx/query-0d4e10f80c1f6d605c70c9d81d0295426d1148e786098b6d1a61a9c7d645c902.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n id as \"id: login::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\",\n password_hash as \"password_hash: StoredHash\"\n from user\n where id = $1\n ", - "describe": { - "columns": [ - { - "name": "id: login::Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "display_name: String", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "canonical_name: String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "created_sequence: Sequence", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_at: DateTime", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "password_hash: StoredHash", - "ordinal": 5, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - false - ] - }, - "hash": "0d4e10f80c1f6d605c70c9d81d0295426d1148e786098b6d1a61a9c7d645c902" -} diff --git a/.sqlx/query-17f6f507d9343a734e1098f6ce56d372edeb35f92769a0181a71d68a68780649.json b/.sqlx/query-17f6f507d9343a734e1098f6ce56d372edeb35f92769a0181a71d68a68780649.json new file mode 100644 index 0000000..0e67b03 --- /dev/null +++ b/.sqlx/query-17f6f507d9343a734e1098f6ce56d372edeb35f92769a0181a71d68a68780649.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n select\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: user::Id\",\n id as \"id: Id\",\n message.body as \"body: Body\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where message.channel = $1\n and deleted.id is null\n ", + "describe": { + "columns": [ + { + "name": "channel: channel::Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "sender: user::Id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "id: Id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "body: Body", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "sent_at: DateTime", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "sent_sequence: Sequence", + "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "deleted_at?: DateTime", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "deleted_sequence?: Sequence", + "ordinal": 7, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "17f6f507d9343a734e1098f6ce56d372edeb35f92769a0181a71d68a68780649" +} diff --git a/.sqlx/query-18aada5bab0b6c438b6b97ab85129c951fb199dadb66f5c980ec30405b74a277.json b/.sqlx/query-18aada5bab0b6c438b6b97ab85129c951fb199dadb66f5c980ec30405b74a277.json new file mode 100644 index 0000000..040ada9 --- /dev/null +++ b/.sqlx/query-18aada5bab0b6c438b6b97ab85129c951fb199dadb66f5c980ec30405b74a277.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n insert into invite (id, issuer, issued_at)\n values ($1, $2, $3)\n returning\n id as \"id: Id\",\n issuer as \"issuer: user::Id\",\n issued_at as \"issued_at: DateTime\"\n ", + "describe": { + "columns": [ + { + "name": "id: Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "issuer: user::Id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "issued_at: DateTime", + "ordinal": 2, + "type_info": "Text" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "18aada5bab0b6c438b6b97ab85129c951fb199dadb66f5c980ec30405b74a277" +} diff --git a/.sqlx/query-1946af14f5d3da9af51fc0e3d4f25cff1556aec7083bc484172c58cbd655a316.json b/.sqlx/query-1946af14f5d3da9af51fc0e3d4f25cff1556aec7083bc484172c58cbd655a316.json deleted file mode 100644 index f765fda..0000000 --- a/.sqlx/query-1946af14f5d3da9af51fc0e3d4f25cff1556aec7083bc484172c58cbd655a316.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n insert into invite (id, issuer, issued_at)\n values ($1, $2, $3)\n returning\n id as \"id: Id\",\n issuer as \"issuer: login::Id\",\n issued_at as \"issued_at: DateTime\"\n ", - "describe": { - "columns": [ - { - "name": "id: Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "issuer: login::Id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "issued_at: DateTime", - "ordinal": 2, - "type_info": "Text" - } - ], - "parameters": { - "Right": 3 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "1946af14f5d3da9af51fc0e3d4f25cff1556aec7083bc484172c58cbd655a316" -} diff --git a/.sqlx/query-2e0aa3126267465ee1ae01e6856eff74a544f0a1c3692766e48a3182df5ada98.json b/.sqlx/query-2e0aa3126267465ee1ae01e6856eff74a544f0a1c3692766e48a3182df5ada98.json deleted file mode 100644 index 227d242..0000000 --- a/.sqlx/query-2e0aa3126267465ee1ae01e6856eff74a544f0a1c3692766e48a3182df5ada98.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n id as \"id: Id\",\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: login::Id\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n message.body as \"body: Body\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where message.sent_at < $1\n and deleted.id is null\n ", - "describe": { - "columns": [ - { - "name": "id: Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "channel: channel::Id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "sender: login::Id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "sent_at: DateTime", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "sent_sequence: Sequence", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "body: Body", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "deleted_at?: DateTime", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "deleted_sequence?: Sequence", - "ordinal": 7, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false - ] - }, - "hash": "2e0aa3126267465ee1ae01e6856eff74a544f0a1c3692766e48a3182df5ada98" -} diff --git a/.sqlx/query-3d0354407ac4dcd7b012328b9086c4f582bd2b3dc99a6a8e5f4853e0d4c43c50.json b/.sqlx/query-3d0354407ac4dcd7b012328b9086c4f582bd2b3dc99a6a8e5f4853e0d4c43c50.json new file mode 100644 index 0000000..0481b7b --- /dev/null +++ b/.sqlx/query-3d0354407ac4dcd7b012328b9086c4f582bd2b3dc99a6a8e5f4853e0d4c43c50.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n select\n id as \"id: Id\",\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: user::Id\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n message.body as \"body: Body\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where message.sent_at < $1\n and deleted.id is null\n ", + "describe": { + "columns": [ + { + "name": "id: Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "channel: channel::Id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "sender: user::Id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "sent_at: DateTime", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "sent_sequence: Sequence", + "ordinal": 4, + "type_info": "Integer" + }, + { + "name": "body: Body", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "deleted_at?: DateTime", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "deleted_sequence?: Sequence", + "ordinal": 7, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "3d0354407ac4dcd7b012328b9086c4f582bd2b3dc99a6a8e5f4853e0d4c43c50" +} diff --git a/.sqlx/query-3ff8a089ca1c57111e8c0e8d6d9da73e88a6cab35ae511674d25aa78bac9bc04.json b/.sqlx/query-3ff8a089ca1c57111e8c0e8d6d9da73e88a6cab35ae511674d25aa78bac9bc04.json new file mode 100644 index 0000000..ad364ea --- /dev/null +++ b/.sqlx/query-3ff8a089ca1c57111e8c0e8d6d9da73e88a6cab35ae511674d25aa78bac9bc04.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n select\n id as \"id: Id\",\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: user::Id\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n message.body as \"body: Body\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where message.last_sequence > $1\n ", + "describe": { + "columns": [ + { + "name": "id: Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "channel: channel::Id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "sender: user::Id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "sent_at: DateTime", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "sent_sequence: Sequence", + "ordinal": 4, + "type_info": "Integer" + }, + { + "name": "body: Body", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "deleted_at?: DateTime", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "deleted_sequence?: Sequence", + "ordinal": 7, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "3ff8a089ca1c57111e8c0e8d6d9da73e88a6cab35ae511674d25aa78bac9bc04" +} diff --git a/.sqlx/query-4623f989492e9eae6788ee9472f1138395b50cdfc9a28e9791e5484890f0201b.json b/.sqlx/query-4623f989492e9eae6788ee9472f1138395b50cdfc9a28e9791e5484890f0201b.json deleted file mode 100644 index bfab6d4..0000000 --- a/.sqlx/query-4623f989492e9eae6788ee9472f1138395b50cdfc9a28e9791e5484890f0201b.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: login::Id\",\n id as \"id: Id\",\n message.body as \"body: Body\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where message.channel = $1\n and deleted.id is null\n ", - "describe": { - "columns": [ - { - "name": "channel: channel::Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "sender: login::Id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "id: Id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "body: Body", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "sent_at: DateTime", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "sent_sequence: Sequence", - "ordinal": 5, - "type_info": "Integer" - }, - { - "name": "deleted_at?: DateTime", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "deleted_sequence?: Sequence", - "ordinal": 7, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false - ] - }, - "hash": "4623f989492e9eae6788ee9472f1138395b50cdfc9a28e9791e5484890f0201b" -} diff --git a/.sqlx/query-53193435a6eeb72266d0e526a3c6adfcfa20b687ac886efe6d41d5abfd7bc183.json b/.sqlx/query-53193435a6eeb72266d0e526a3c6adfcfa20b687ac886efe6d41d5abfd7bc183.json new file mode 100644 index 0000000..31c14de --- /dev/null +++ b/.sqlx/query-53193435a6eeb72266d0e526a3c6adfcfa20b687ac886efe6d41d5abfd7bc183.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n select\n invite.id as \"invite_id: Id\",\n issuer.id as \"issuer_id: user::Id\",\n issuer.display_name as \"issuer_name: nfc::String\",\n invite.issued_at as \"invite_issued_at: DateTime\"\n from invite\n join user as issuer on (invite.issuer = issuer.id)\n where invite.id = $1\n ", + "describe": { + "columns": [ + { + "name": "invite_id: Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "issuer_id: user::Id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "issuer_name: nfc::String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "invite_issued_at: DateTime", + "ordinal": 3, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "53193435a6eeb72266d0e526a3c6adfcfa20b687ac886efe6d41d5abfd7bc183" +} diff --git a/.sqlx/query-53b1f14d450a99f486bfd79075e71bd7e30dc93d32e1f273c878f18f2984860f.json b/.sqlx/query-53b1f14d450a99f486bfd79075e71bd7e30dc93d32e1f273c878f18f2984860f.json deleted file mode 100644 index 7ec6aac..0000000 --- a/.sqlx/query-53b1f14d450a99f486bfd79075e71bd7e30dc93d32e1f273c878f18f2984860f.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n id as \"id: Id\",\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: login::Id\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n message.body as \"body: Body\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where message.last_sequence > $1\n ", - "describe": { - "columns": [ - { - "name": "id: Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "channel: channel::Id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "sender: login::Id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "sent_at: DateTime", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "sent_sequence: Sequence", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "body: Body", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "deleted_at?: DateTime", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "deleted_sequence?: Sequence", - "ordinal": 7, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false - ] - }, - "hash": "53b1f14d450a99f486bfd79075e71bd7e30dc93d32e1f273c878f18f2984860f" -} diff --git a/.sqlx/query-579bf0557d3e141dfd411c25c2ae66d6abb70f7bd2413cfbe23b71d1ce6090cd.json b/.sqlx/query-579bf0557d3e141dfd411c25c2ae66d6abb70f7bd2413cfbe23b71d1ce6090cd.json new file mode 100644 index 0000000..27349d7 --- /dev/null +++ b/.sqlx/query-579bf0557d3e141dfd411c25c2ae66d6abb70f7bd2413cfbe23b71d1ce6090cd.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "\n insert into message\n (id, channel, sender, sent_at, sent_sequence, body, last_sequence)\n values ($1, $2, $3, $4, $5, $6, $7)\n returning\n id as \"id: Id\",\n channel as \"channel: channel::Id\",\n sender as \"sender: user::Id\",\n sent_at as \"sent_at: DateTime\",\n sent_sequence as \"sent_sequence: Sequence\",\n body as \"body: Body\"\n ", + "describe": { + "columns": [ + { + "name": "id: Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "channel: channel::Id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "sender: user::Id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "sent_at: DateTime", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "sent_sequence: Sequence", + "ordinal": 4, + "type_info": "Integer" + }, + { + "name": "body: Body", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 7 + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "579bf0557d3e141dfd411c25c2ae66d6abb70f7bd2413cfbe23b71d1ce6090cd" +} diff --git a/.sqlx/query-5f9dfdff6e5067a02f9f01826c2d05f1b173691846e36370bccf166c08ce1955.json b/.sqlx/query-5f9dfdff6e5067a02f9f01826c2d05f1b173691846e36370bccf166c08ce1955.json deleted file mode 100644 index 215c0b5..0000000 --- a/.sqlx/query-5f9dfdff6e5067a02f9f01826c2d05f1b173691846e36370bccf166c08ce1955.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n id as \"id: login::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\",\n password_hash as \"password_hash: StoredHash\"\n from user\n where canonical_name = $1\n ", - "describe": { - "columns": [ - { - "name": "id: login::Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "display_name: String", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "canonical_name: String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "created_sequence: Sequence", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_at: DateTime", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "password_hash: StoredHash", - "ordinal": 5, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - false - ] - }, - "hash": "5f9dfdff6e5067a02f9f01826c2d05f1b173691846e36370bccf166c08ce1955" -} diff --git a/.sqlx/query-6755a7bba5733f4a8090bc895dff436d67edf6669a555fe20b178e9ba3039b0c.json b/.sqlx/query-6755a7bba5733f4a8090bc895dff436d67edf6669a555fe20b178e9ba3039b0c.json deleted file mode 100644 index 94bc39f..0000000 --- a/.sqlx/query-6755a7bba5733f4a8090bc895dff436d67edf6669a555fe20b178e9ba3039b0c.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n id as \"id: login::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from user\n where id = $1\n ", - "describe": { - "columns": [ - { - "name": "id: login::Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "display_name: String", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "canonical_name: String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "created_sequence: Sequence", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "created_at: DateTime", - "ordinal": 4, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false - ] - }, - "hash": "6755a7bba5733f4a8090bc895dff436d67edf6669a555fe20b178e9ba3039b0c" -} diff --git a/.sqlx/query-684b4bef3e553b8ccfa20d8de3272e44b1c15ed75977ce24f25fd62042fb20fb.json b/.sqlx/query-684b4bef3e553b8ccfa20d8de3272e44b1c15ed75977ce24f25fd62042fb20fb.json new file mode 100644 index 0000000..d1c2732 --- /dev/null +++ b/.sqlx/query-684b4bef3e553b8ccfa20d8de3272e44b1c15ed75977ce24f25fd62042fb20fb.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n update token\n set last_used_at = $1\n where secret = $2\n returning\n id as \"token: Id\",\n user as \"user: user::Id\"\n ", + "describe": { + "columns": [ + { + "name": "token: Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "user: user::Id", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false + ] + }, + "hash": "684b4bef3e553b8ccfa20d8de3272e44b1c15ed75977ce24f25fd62042fb20fb" +} diff --git a/.sqlx/query-713ccbb31289eebd93840a7cf99b01395f30788ab60232f239ce85125e425b79.json b/.sqlx/query-713ccbb31289eebd93840a7cf99b01395f30788ab60232f239ce85125e425b79.json new file mode 100644 index 0000000..63ed2eb --- /dev/null +++ b/.sqlx/query-713ccbb31289eebd93840a7cf99b01395f30788ab60232f239ce85125e425b79.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n select\n id as \"id: Id\",\n issuer as \"issuer: user::Id\",\n issued_at as \"issued_at: DateTime\"\n from invite\n where id = $1\n ", + "describe": { + "columns": [ + { + "name": "id: Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "issuer: user::Id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "issued_at: DateTime", + "ordinal": 2, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "713ccbb31289eebd93840a7cf99b01395f30788ab60232f239ce85125e425b79" +} diff --git a/.sqlx/query-72441293731853e9f0cc1141e4322f8026f9e2515b6bacaed81f6248c52a198a.json b/.sqlx/query-72441293731853e9f0cc1141e4322f8026f9e2515b6bacaed81f6248c52a198a.json deleted file mode 100644 index eb30352..0000000 --- a/.sqlx/query-72441293731853e9f0cc1141e4322f8026f9e2515b6bacaed81f6248c52a198a.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n insert into message\n (id, channel, sender, sent_at, sent_sequence, body, last_sequence)\n values ($1, $2, $3, $4, $5, $6, $7)\n returning\n id as \"id: Id\",\n channel as \"channel: channel::Id\",\n sender as \"sender: login::Id\",\n sent_at as \"sent_at: DateTime\",\n sent_sequence as \"sent_sequence: Sequence\",\n body as \"body: Body\"\n ", - "describe": { - "columns": [ - { - "name": "id: Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "channel: channel::Id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "sender: login::Id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "sent_at: DateTime", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "sent_sequence: Sequence", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "body: Body", - "ordinal": 5, - "type_info": "Text" - } - ], - "parameters": { - "Right": 7 - }, - "nullable": [ - false, - false, - false, - false, - false, - true - ] - }, - "hash": "72441293731853e9f0cc1141e4322f8026f9e2515b6bacaed81f6248c52a198a" -} diff --git a/.sqlx/query-78d24fa907f3dcc0c129880e83b4ef41bad03b57937a27f98aebbef5268ef5f5.json b/.sqlx/query-78d24fa907f3dcc0c129880e83b4ef41bad03b57937a27f98aebbef5268ef5f5.json deleted file mode 100644 index 09440ca..0000000 --- a/.sqlx/query-78d24fa907f3dcc0c129880e83b4ef41bad03b57937a27f98aebbef5268ef5f5.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: login::Id\",\n id as \"id: Id\",\n message.body as \"body: Body\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where id = $1\n ", - "describe": { - "columns": [ - { - "name": "channel: channel::Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "sender: login::Id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "id: Id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "body: Body", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "sent_at: DateTime", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "sent_sequence: Sequence", - "ordinal": 5, - "type_info": "Integer" - }, - { - "name": "deleted_at?: DateTime", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "deleted_sequence?: Sequence", - "ordinal": 7, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false - ] - }, - "hash": "78d24fa907f3dcc0c129880e83b4ef41bad03b57937a27f98aebbef5268ef5f5" -} diff --git a/.sqlx/query-79495da0101ad12e7e983666b2ca84cbe31458ff73a94648c30b62f3130a5a8b.json b/.sqlx/query-79495da0101ad12e7e983666b2ca84cbe31458ff73a94648c30b62f3130a5a8b.json new file mode 100644 index 0000000..8f9be21 --- /dev/null +++ b/.sqlx/query-79495da0101ad12e7e983666b2ca84cbe31458ff73a94648c30b62f3130a5a8b.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "\n select\n id as \"id: user::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\",\n password_hash as \"password_hash: StoredHash\"\n from user\n where canonical_name = $1\n ", + "describe": { + "columns": [ + { + "name": "id: user::Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "display_name: String", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "canonical_name: String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_sequence: Sequence", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "created_at: DateTime", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "password_hash: StoredHash", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "79495da0101ad12e7e983666b2ca84cbe31458ff73a94648c30b62f3130a5a8b" +} diff --git a/.sqlx/query-9cf211b1f37708fc8f19b3af911f7488a708d5a50920bb042c0229c314ee3281.json b/.sqlx/query-9cf211b1f37708fc8f19b3af911f7488a708d5a50920bb042c0229c314ee3281.json new file mode 100644 index 0000000..0926b67 --- /dev/null +++ b/.sqlx/query-9cf211b1f37708fc8f19b3af911f7488a708d5a50920bb042c0229c314ee3281.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n select\n id as \"id: user::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from user\n where id = $1\n ", + "describe": { + "columns": [ + { + "name": "id: user::Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "display_name: String", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "canonical_name: String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_sequence: Sequence", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "created_at: DateTime", + "ordinal": 4, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "9cf211b1f37708fc8f19b3af911f7488a708d5a50920bb042c0229c314ee3281" +} diff --git a/.sqlx/query-afc6db503a3c49c18a9cb07cf0360789d46d94d9cd98887a3d9660d9b753d416.json b/.sqlx/query-afc6db503a3c49c18a9cb07cf0360789d46d94d9cd98887a3d9660d9b753d416.json new file mode 100644 index 0000000..b7e6c1b --- /dev/null +++ b/.sqlx/query-afc6db503a3c49c18a9cb07cf0360789d46d94d9cd98887a3d9660d9b753d416.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "\n select\n id as \"id: user::Id\",\n display_name as \"display_name: String\",\n canonical_name as \"canonical_name: String\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\",\n password_hash as \"password_hash: StoredHash\"\n from user\n where id = $1\n ", + "describe": { + "columns": [ + { + "name": "id: user::Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "display_name: String", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "canonical_name: String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_sequence: Sequence", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "created_at: DateTime", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "password_hash: StoredHash", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "afc6db503a3c49c18a9cb07cf0360789d46d94d9cd98887a3d9660d9b753d416" +} diff --git a/.sqlx/query-b18432e78891ffca0d7f3fcd1c543db4a3f02c211462704f7810fdbed924ac30.json b/.sqlx/query-b18432e78891ffca0d7f3fcd1c543db4a3f02c211462704f7810fdbed924ac30.json new file mode 100644 index 0000000..3ae7605 --- /dev/null +++ b/.sqlx/query-b18432e78891ffca0d7f3fcd1c543db4a3f02c211462704f7810fdbed924ac30.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n select\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: user::Id\",\n message.id as \"id: Id\",\n message.body as \"body: Body\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where message.sent_sequence <= $1\n order by message.sent_sequence\n ", + "describe": { + "columns": [ + { + "name": "channel: channel::Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "sender: user::Id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "id: Id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "body: Body", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "sent_at: DateTime", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "sent_sequence: Sequence", + "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "deleted_at?: DateTime", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "deleted_sequence?: Sequence", + "ordinal": 7, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "b18432e78891ffca0d7f3fcd1c543db4a3f02c211462704f7810fdbed924ac30" +} diff --git a/.sqlx/query-bb1be4fab0d7fa56ae5a8ea977d13047cefdebee841d0df3c671a2104b9aef8f.json b/.sqlx/query-bb1be4fab0d7fa56ae5a8ea977d13047cefdebee841d0df3c671a2104b9aef8f.json deleted file mode 100644 index a32a555..0000000 --- a/.sqlx/query-bb1be4fab0d7fa56ae5a8ea977d13047cefdebee841d0df3c671a2104b9aef8f.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n update token\n set last_used_at = $1\n where secret = $2\n returning\n id as \"token: Id\",\n user as \"user: login::Id\"\n ", - "describe": { - "columns": [ - { - "name": "token: Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "user: login::Id", - "ordinal": 1, - "type_info": "Text" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false - ] - }, - "hash": "bb1be4fab0d7fa56ae5a8ea977d13047cefdebee841d0df3c671a2104b9aef8f" -} diff --git a/.sqlx/query-c58e61c57373a7f19d98c46379d16a29b4d0d1348e31599dc47522849f759066.json b/.sqlx/query-c58e61c57373a7f19d98c46379d16a29b4d0d1348e31599dc47522849f759066.json deleted file mode 100644 index 18d720c..0000000 --- a/.sqlx/query-c58e61c57373a7f19d98c46379d16a29b4d0d1348e31599dc47522849f759066.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n invite.id as \"invite_id: Id\",\n issuer.id as \"issuer_id: login::Id\",\n issuer.display_name as \"issuer_name: nfc::String\",\n invite.issued_at as \"invite_issued_at: DateTime\"\n from invite\n join user as issuer on (invite.issuer = issuer.id)\n where invite.id = $1\n ", - "describe": { - "columns": [ - { - "name": "invite_id: Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "issuer_id: login::Id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "issuer_name: nfc::String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "invite_issued_at: DateTime", - "ordinal": 3, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "c58e61c57373a7f19d98c46379d16a29b4d0d1348e31599dc47522849f759066" -} diff --git a/.sqlx/query-d49d4ab5f1bf4c78fa619680b04a506cd63a85741923f841b7c36c46b70a538f.json b/.sqlx/query-d49d4ab5f1bf4c78fa619680b04a506cd63a85741923f841b7c36c46b70a538f.json deleted file mode 100644 index 3ec71e8..0000000 --- a/.sqlx/query-d49d4ab5f1bf4c78fa619680b04a506cd63a85741923f841b7c36c46b70a538f.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n id as \"id: Id\",\n issuer as \"issuer: login::Id\",\n issued_at as \"issued_at: DateTime\"\n from invite\n where id = $1\n ", - "describe": { - "columns": [ - { - "name": "id: Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "issuer: login::Id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "issued_at: DateTime", - "ordinal": 2, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "d49d4ab5f1bf4c78fa619680b04a506cd63a85741923f841b7c36c46b70a538f" -} diff --git a/.sqlx/query-fb7114754c6dc8ffe623ae0f3f61ec2e20795692db7019d962378c740ae69599.json b/.sqlx/query-fb7114754c6dc8ffe623ae0f3f61ec2e20795692db7019d962378c740ae69599.json new file mode 100644 index 0000000..7f1e1f3 --- /dev/null +++ b/.sqlx/query-fb7114754c6dc8ffe623ae0f3f61ec2e20795692db7019d962378c740ae69599.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n select\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: user::Id\",\n id as \"id: Id\",\n message.body as \"body: Body\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where id = $1\n ", + "describe": { + "columns": [ + { + "name": "channel: channel::Id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "sender: user::Id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "id: Id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "body: Body", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "sent_at: DateTime", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "sent_sequence: Sequence", + "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "deleted_at?: DateTime", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "deleted_sequence?: Sequence", + "ordinal": 7, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "fb7114754c6dc8ffe623ae0f3f61ec2e20795692db7019d962378c740ae69599" +} diff --git a/.sqlx/query-ff61ff22108f1e98bbfc9a84a27bdaefca99706a0c73c17f19cc537f3f669882.json b/.sqlx/query-ff61ff22108f1e98bbfc9a84a27bdaefca99706a0c73c17f19cc537f3f669882.json deleted file mode 100644 index f38f49c..0000000 --- a/.sqlx/query-ff61ff22108f1e98bbfc9a84a27bdaefca99706a0c73c17f19cc537f3f669882.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n select\n message.channel as \"channel: channel::Id\",\n message.sender as \"sender: login::Id\",\n message.id as \"id: Id\",\n message.body as \"body: Body\",\n message.sent_at as \"sent_at: DateTime\",\n message.sent_sequence as \"sent_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from message\n left join message_deleted as deleted\n using (id)\n where message.sent_sequence <= $1\n order by message.sent_sequence\n ", - "describe": { - "columns": [ - { - "name": "channel: channel::Id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "sender: login::Id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "id: Id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "body: Body", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "sent_at: DateTime", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "sent_sequence: Sequence", - "ordinal": 5, - "type_info": "Integer" - }, - { - "name": "deleted_at?: DateTime", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "deleted_sequence?: Sequence", - "ordinal": 7, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false - ] - }, - "hash": "ff61ff22108f1e98bbfc9a84a27bdaefca99706a0c73c17f19cc537f3f669882" -} diff --git a/src/app.rs b/src/app.rs index 0dbf017..b7e52a4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,7 +11,7 @@ use crate::{ }; #[cfg(test)] -use crate::login::app::Logins; +use crate::user::app::Users; #[derive(Clone)] pub struct App { @@ -50,8 +50,8 @@ impl App { } #[cfg(test)] - pub const fn logins(&self) -> Logins { - Logins::new(&self.db, &self.events) + pub const fn users(&self) -> Users { + Users::new(&self.db, &self.events) } pub const fn messages(&self) -> Messages { diff --git a/src/boot/app.rs b/src/boot/app.rs index 909f7d8..9c2559e 100644 --- a/src/boot/app.rs +++ b/src/boot/app.rs @@ -4,9 +4,9 @@ use super::Snapshot; use crate::{ channel::{self, repo::Provider as _}, event::repo::Provider as _, - login::{self, repo::Provider as _}, message::repo::Provider as _, name, + user::{self, repo::Provider as _}, }; pub struct Boot<'a> { @@ -22,7 +22,7 @@ impl<'a> Boot<'a> { let mut tx = self.db.begin().await?; let resume_point = tx.sequence().current().await?; - let logins = tx.logins().all(resume_point).await?; + let logins = tx.users().all(resume_point).await?; let channels = tx.channels().all(resume_point).await?; let messages = tx.messages().all(resume_point).await?; @@ -59,9 +59,9 @@ pub enum Error { Database(#[from] sqlx::Error), } -impl From for Error { - fn from(error: login::repo::LoadError) -> Self { - use login::repo::LoadError; +impl From for Error { + fn from(error: user::repo::LoadError) -> Self { + use user::repo::LoadError; match error { LoadError::Name(error) => error.into(), LoadError::Database(error) => error.into(), diff --git a/src/boot/mod.rs b/src/boot/mod.rs index ed4764a..d614df5 100644 --- a/src/boot/mod.rs +++ b/src/boot/mod.rs @@ -1,14 +1,14 @@ pub mod app; mod routes; -use crate::{channel::Channel, event::Sequence, login::Login, message::Message}; +use crate::{channel::Channel, event::Sequence, message::Message, user::User}; pub use self::routes::router; #[derive(serde::Serialize)] pub struct Snapshot { pub resume_point: Sequence, - pub logins: Vec, + pub logins: Vec, pub channels: Vec, pub messages: Vec, } diff --git a/src/boot/routes/get.rs b/src/boot/routes/get.rs index 563fbf1..c04c6b3 100644 --- a/src/boot/routes/get.rs +++ b/src/boot/routes/get.rs @@ -3,19 +3,19 @@ use axum::{ response::{self, IntoResponse}, }; -use crate::{app::App, boot::Snapshot, error::Internal, login::Login, token::extract::Identity}; +use crate::{app::App, boot::Snapshot, error::Internal, token::extract::Identity, user::User}; pub async fn handler(State(app): State, identity: Identity) -> Result { let snapshot = app.boot().snapshot().await?; Ok(Response { - login: identity.login, + login: identity.user, snapshot, }) } #[derive(serde::Serialize)] pub struct Response { - pub login: Login, + pub login: User, #[serde(flatten)] pub snapshot: Snapshot, } diff --git a/src/boot/routes/test.rs b/src/boot/routes/test.rs index 202dcb9..5bd9f66 100644 --- a/src/boot/routes/test.rs +++ b/src/boot/routes/test.rs @@ -12,7 +12,7 @@ async fn returns_identity() { .await .expect("boot always succeeds"); - assert_eq!(viewer.login, response.login); + assert_eq!(viewer.user, response.login); } #[tokio::test] diff --git a/src/channel/routes/channel/post.rs b/src/channel/routes/channel/post.rs index 3f14d64..0aad5e5 100644 --- a/src/channel/routes/channel/post.rs +++ b/src/channel/routes/channel/post.rs @@ -21,7 +21,7 @@ pub async fn handler( ) -> Result { let message = app .messages() - .send(&channel, &identity.login, &sent_at, &request.body) + .send(&channel, &identity.user, &sent_at, &request.body) .await?; Ok(Response(message)) diff --git a/src/channel/routes/channel/test/post.rs b/src/channel/routes/channel/test/post.rs index bc0684b..d9527ac 100644 --- a/src/channel/routes/channel/test/post.rs +++ b/src/channel/routes/channel/test/post.rs @@ -55,7 +55,7 @@ async fn messages_in_order() { .await { assert_eq!(*sent_at, event.at()); - assert_eq!(sender.login.id, event.message.sender); + assert_eq!(sender.user.id, event.message.sender); assert_eq!(body, event.message.body); } } diff --git a/src/cli.rs b/src/cli.rs index 775df7f..4232c00 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -17,9 +17,9 @@ use tokio::net; use crate::{ app::App, - boot, channel, clock, db, event, expire, invite, login, message, + boot, channel, clock, db, event, expire, invite, message, setup::{self, middleware::setup_required}, - ui, + ui, user, }; /// Command-line entry point for running the `pilcrow` server. @@ -136,7 +136,7 @@ fn routers(app: &App) -> Router { channel::router(), event::router(), invite::router(), - login::router(), + user::router(), message::router(), ] .into_iter() diff --git a/src/event/app.rs b/src/event/app.rs index 8661c90..447a98f 100644 --- a/src/event/app.rs +++ b/src/event/app.rs @@ -8,9 +8,9 @@ use sqlx::sqlite::SqlitePool; use super::{Event, Sequence, Sequenced, broadcaster::Broadcaster}; use crate::{ channel::{self, repo::Provider as _}, - login::{self, repo::Provider as _}, message::{self, repo::Provider as _}, name, + user::{self, repo::Provider as _}, }; pub struct Events<'a> { @@ -33,10 +33,10 @@ impl<'a> Events<'a> { let mut tx = self.db.begin().await?; - let logins = tx.logins().replay(resume_at).await?; + let logins = tx.users().replay(resume_at).await?; let login_events = logins .iter() - .map(login::History::events) + .map(user::History::events) .kmerge_by(Sequence::merge) .filter(Sequence::after(resume_at)) .map(Event::from); @@ -88,9 +88,9 @@ pub enum Error { Name(#[from] name::Error), } -impl From for Error { - fn from(error: login::repo::LoadError) -> Self { - use login::repo::LoadError; +impl From for Error { + fn from(error: user::repo::LoadError) -> Self { + use user::repo::LoadError; match error { LoadError::Database(error) => error.into(), LoadError::Name(error) => error.into(), diff --git a/src/event/mod.rs b/src/event/mod.rs index 9996916..773adc3 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -1,4 +1,4 @@ -use crate::{channel, login, message}; +use crate::{channel, message, user}; pub mod app; mod broadcaster; @@ -16,7 +16,7 @@ pub use self::{ #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum Event { - Login(login::Event), + Login(user::Event), Channel(channel::Event), Message(message::Event), } @@ -31,8 +31,8 @@ impl Sequenced for Event { } } -impl From for Event { - fn from(event: login::Event) -> Self { +impl From for Event { + fn from(event: user::Event) -> Self { Self::Login(event) } } diff --git a/src/event/routes/test/invite.rs b/src/event/routes/test/invite.rs index 73af62d..80b4291 100644 --- a/src/event/routes/test/invite.rs +++ b/src/event/routes/test/invite.rs @@ -42,7 +42,7 @@ async fn accepting_invite() { let _ = events .filter_map(fixtures::event::login) .filter_map(fixtures::event::login::created) - .filter(|event| future::ready(event.login == joiner)) + .filter(|event| future::ready(event.user == joiner)) .next() .expect_some("a login created event is sent") .await; @@ -83,7 +83,7 @@ async fn previously_accepted_invite() { let _ = events .filter_map(fixtures::event::login) .filter_map(fixtures::event::login::created) - .filter(|event| future::ready(event.login == joiner)) + .filter(|event| future::ready(event.user == joiner)) .next() .expect_some("a login created event is sent") .await; diff --git a/src/event/routes/test/setup.rs b/src/event/routes/test/setup.rs index 26b7ea7..345018e 100644 --- a/src/event/routes/test/setup.rs +++ b/src/event/routes/test/setup.rs @@ -43,7 +43,7 @@ async fn previously_completed() { let _ = events .filter_map(fixtures::event::login) .filter_map(fixtures::event::login::created) - .filter(|event| future::ready(event.login == owner)) + .filter(|event| future::ready(event.user == owner)) .next() .expect_some("a login created event is sent") .await; diff --git a/src/event/routes/test/token.rs b/src/event/routes/test/token.rs index fa76865..d2232a4 100644 --- a/src/event/routes/test/token.rs +++ b/src/event/routes/test/token.rs @@ -129,7 +129,7 @@ async fn terminates_on_password_change() { let (_, password) = creds; let to = fixtures::login::propose_password(); app.tokens() - .change_password(&subscriber.login, &password, &to, &fixtures::now()) + .change_password(&subscriber.user, &password, &to, &fixtures::now()) .await .expect("expiring tokens succeeds"); diff --git a/src/invite/app.rs b/src/invite/app.rs index c56c9b3..e7bd5c6 100644 --- a/src/invite/app.rs +++ b/src/invite/app.rs @@ -6,12 +6,12 @@ use crate::{ clock::DateTime, db::{Duplicate as _, NotFound as _}, event::Broadcaster, - login::{ - Login, Password, - create::{self, Create}, - }, name::Name, token::{Secret, repo::Provider as _}, + user::{ + Password, User, + create::{self, Create}, + }, }; pub struct Invites<'a> { @@ -24,7 +24,7 @@ impl<'a> Invites<'a> { Self { db, events } } - pub async fn issue(&self, issuer: &Login, issued_at: &DateTime) -> Result { + pub async fn issue(&self, issuer: &User, issued_at: &DateTime) -> Result { let mut tx = self.db.begin().await?; let invite = tx.invites().create(issuer, issued_at).await?; tx.commit().await?; @@ -46,7 +46,7 @@ impl<'a> Invites<'a> { name: &Name, password: &Password, accepted_at: &DateTime, - ) -> Result<(Login, Secret), AcceptError> { + ) -> Result<(User, Secret), AcceptError> { let create = Create::begin(name, password, accepted_at); let mut tx = self.db.begin().await?; @@ -70,7 +70,7 @@ impl<'a> Invites<'a> { .store(&mut tx) .await .duplicate(|| AcceptError::DuplicateLogin(name.clone()))?; - let secret = tx.tokens().issue(stored.login(), accepted_at).await?; + let secret = tx.tokens().issue(stored.user(), accepted_at).await?; tx.commit().await?; let login = stored.publish(self.events); @@ -94,7 +94,7 @@ impl<'a> Invites<'a> { pub enum AcceptError { #[error("invite not found: {0}")] NotFound(Id), - #[error("invalid login name: {0}")] + #[error("invalid user name: {0}")] InvalidName(Name), #[error("name in use: {0}")] DuplicateLogin(Name), diff --git a/src/invite/mod.rs b/src/invite/mod.rs index 53ca984..2d32fda 100644 --- a/src/invite/mod.rs +++ b/src/invite/mod.rs @@ -3,14 +3,14 @@ mod id; mod repo; mod routes; -use crate::{clock::DateTime, login, normalize::nfc}; +use crate::{clock::DateTime, normalize::nfc, user}; pub use self::{id::Id, routes::router}; #[derive(Debug, serde::Serialize)] pub struct Invite { pub id: Id, - pub issuer: login::Id, + pub issuer: user::Id, pub issued_at: DateTime, } diff --git a/src/invite/repo.rs b/src/invite/repo.rs index c716ed9..79114ec 100644 --- a/src/invite/repo.rs +++ b/src/invite/repo.rs @@ -3,8 +3,8 @@ use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; use super::{Id, Invite, Summary}; use crate::{ clock::DateTime, - login::{self, Login}, normalize::nfc, + user::{self, User}, }; pub trait Provider { @@ -22,7 +22,7 @@ pub struct Invites<'t>(&'t mut SqliteConnection); impl Invites<'_> { pub async fn create( &mut self, - issuer: &Login, + issuer: &User, issued_at: &DateTime, ) -> Result { let id = Id::generate(); @@ -33,7 +33,7 @@ impl Invites<'_> { values ($1, $2, $3) returning id as "id: Id", - issuer as "issuer: login::Id", + issuer as "issuer: user::Id", issued_at as "issued_at: DateTime" "#, id, @@ -52,7 +52,7 @@ impl Invites<'_> { r#" select id as "id: Id", - issuer as "issuer: login::Id", + issuer as "issuer: user::Id", issued_at as "issued_at: DateTime" from invite where id = $1 @@ -70,7 +70,7 @@ impl Invites<'_> { r#" select invite.id as "invite_id: Id", - issuer.id as "issuer_id: login::Id", + issuer.id as "issuer_id: user::Id", issuer.display_name as "issuer_name: nfc::String", invite.issued_at as "invite_issued_at: DateTime" from invite diff --git a/src/invite/routes/invite/post.rs b/src/invite/routes/invite/post.rs index bb68e07..58d15c2 100644 --- a/src/invite/routes/invite/post.rs +++ b/src/invite/routes/invite/post.rs @@ -9,9 +9,9 @@ use crate::{ clock::RequestedAt, error::{Internal, NotFound}, invite::app, - login::{Login, Password}, name::Name, token::extract::IdentityCookie, + user::{Password, User}, }; pub async fn handler( @@ -20,7 +20,7 @@ pub async fn handler( identity: IdentityCookie, Path(invite): Path, Json(request): Json, -) -> Result<(IdentityCookie, Json), Error> { +) -> Result<(IdentityCookie, Json), Error> { let (login, secret) = app .invites() .accept(&invite, &request.name, &request.password, &accepted_at) diff --git a/src/invite/routes/invite/test/post.rs b/src/invite/routes/invite/test/post.rs index 40e0580..3db764c 100644 --- a/src/invite/routes/invite/test/post.rs +++ b/src/invite/routes/invite/test/post.rs @@ -171,14 +171,14 @@ async fn conflicting_name() { let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await; let existing_name = Name::from("rijksmuseum"); - app.logins() + app.users() .create( &existing_name, &fixtures::login::propose_password(), &fixtures::now(), ) .await - .expect("creating a login in an empty environment succeeds"); + .expect("creating a user in an empty environment succeeds"); // Call the endpoint diff --git a/src/invite/routes/post.rs b/src/invite/routes/post.rs index 898081e..f7ca76c 100644 --- a/src/invite/routes/post.rs +++ b/src/invite/routes/post.rs @@ -10,7 +10,7 @@ pub async fn handler( identity: Identity, _: Json, ) -> Result, Internal> { - let invite = app.invites().issue(&identity.login, &issued_at).await?; + let invite = app.invites().issue(&identity.user, &issued_at).await?; Ok(Json(invite)) } diff --git a/src/invite/routes/test.rs b/src/invite/routes/test.rs index 4d99660..4ea8a3d 100644 --- a/src/invite/routes/test.rs +++ b/src/invite/routes/test.rs @@ -23,6 +23,6 @@ async fn create_invite() { .expect("creating an invite always succeeds"); // Verify the response - assert_eq!(issuer.login.id, invite.issuer); + assert_eq!(issuer.user.id, invite.issuer); assert_eq!(&*issued_at, &invite.issued_at); } diff --git a/src/lib.rs b/src/lib.rs index 765e625..4cce63b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,6 @@ mod event; mod expire; mod id; mod invite; -mod login; mod message; mod name; mod normalize; @@ -23,3 +22,4 @@ mod setup; mod test; mod token; mod ui; +mod user; diff --git a/src/login/app.rs b/src/login/app.rs deleted file mode 100644 index 2da4d6a..0000000 --- a/src/login/app.rs +++ /dev/null @@ -1,56 +0,0 @@ -use sqlx::sqlite::SqlitePool; - -use super::{ - Login, Password, - create::{self, Create}, -}; -use crate::{clock::DateTime, event::Broadcaster, name::Name}; - -pub struct Logins<'a> { - db: &'a SqlitePool, - events: &'a Broadcaster, -} - -impl<'a> Logins<'a> { - pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { - Self { db, events } - } - - pub async fn create( - &self, - name: &Name, - password: &Password, - created_at: &DateTime, - ) -> Result { - let create = Create::begin(name, password, created_at); - let validated = create.validate()?; - - let mut tx = self.db.begin().await?; - let stored = validated.store(&mut tx).await?; - tx.commit().await?; - - let login = stored.publish(self.events); - - Ok(login.as_created()) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum CreateError { - #[error("invalid login name: {0}")] - InvalidName(Name), - #[error(transparent)] - PasswordHash(#[from] password_hash::Error), - #[error(transparent)] - Database(#[from] sqlx::Error), -} - -#[cfg(test)] -impl From for CreateError { - fn from(error: create::Error) -> Self { - match error { - create::Error::InvalidName(name) => Self::InvalidName(name), - create::Error::PasswordHash(error) => Self::PasswordHash(error), - } - } -} diff --git a/src/login/create.rs b/src/login/create.rs deleted file mode 100644 index c5cea08..0000000 --- a/src/login/create.rs +++ /dev/null @@ -1,95 +0,0 @@ -use sqlx::{sqlite::Sqlite, Transaction}; - -use super::{password::StoredHash, repo::Provider as _, validate, History, Password}; -use crate::{ - clock::DateTime, - event::{repo::Provider as _, Broadcaster, Event}, - name::Name, -}; - -#[must_use = "dropping a login creation attempt is likely a mistake"] -pub struct Create<'a> { - name: &'a Name, - password: &'a Password, - created_at: &'a DateTime, -} - -impl<'a> Create<'a> { - pub fn begin(name: &'a Name, password: &'a Password, created_at: &'a DateTime) -> Self { - Self { - name, - password, - created_at, - } - } - - pub fn validate(self) -> Result, Error> { - let Self { - name, - password, - created_at, - } = self; - - if !validate::name(name) { - return Err(Error::InvalidName(name.clone())); - } - - let password_hash = password.hash()?; - - Ok(Validated { - name, - password_hash, - created_at, - }) - } -} - -#[must_use = "dropping a login creation attempt is likely a mistake"] -pub struct Validated<'a> { - name: &'a Name, - password_hash: StoredHash, - created_at: &'a DateTime, -} - -impl Validated<'_> { - pub async fn store(self, tx: &mut Transaction<'_, Sqlite>) -> Result { - let Self { - name, - password_hash, - created_at, - } = self; - - let created = tx.sequence().next(created_at).await?; - let login = tx.logins().create(name, &password_hash, &created).await?; - - Ok(Stored { login }) - } -} - -#[must_use = "dropping a login creation attempt is likely a mistake"] -pub struct Stored { - login: History, -} - -impl Stored { - #[must_use = "dropping a login creation attempt is likely a mistake"] - pub fn publish(self, events: &Broadcaster) -> History { - let Self { login } = self; - - events.broadcast(login.events().map(Event::from).collect::>()); - - login - } - - pub fn login(&self) -> &History { - &self.login - } -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("invalid login name: {0}")] - InvalidName(Name), - #[error(transparent)] - PasswordHash(#[from] password_hash::Error), -} diff --git a/src/login/event.rs b/src/login/event.rs deleted file mode 100644 index b03451a..0000000 --- a/src/login/event.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::snapshot::Login; -use crate::event::{Instant, Sequenced}; - -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -#[serde(tag = "event", rename_all = "snake_case")] -pub enum Event { - Created(Created), -} - -impl Sequenced for Event { - fn instant(&self) -> Instant { - match self { - Self::Created(created) => created.instant(), - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -pub struct Created { - #[serde(flatten)] - pub instant: Instant, - #[serde(flatten)] - pub login: Login, -} - -impl Sequenced for Created { - fn instant(&self) -> Instant { - self.instant - } -} - -impl From for Event { - fn from(event: Created) -> Self { - Self::Created(event) - } -} diff --git a/src/login/history.rs b/src/login/history.rs deleted file mode 100644 index d67bcce..0000000 --- a/src/login/history.rs +++ /dev/null @@ -1,52 +0,0 @@ -use super::{ - Id, Login, - event::{Created, Event}, -}; -use crate::event::{Instant, Sequence}; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct History { - pub login: Login, - pub created: Instant, -} - -// State interface -impl History { - pub fn id(&self) -> &Id { - &self.login.id - } - - // Snapshot of this login as it was when created. (Note to the future: it's okay - // if this returns a redacted or modified version of the login. If we implement - // renames by redacting the original name, then this should return the edited - // login, not the original, even if that's not how it was "as created.") - pub fn as_created(&self) -> Login { - self.login.clone() - } - - pub fn as_of(&self, resume_point: Sequence) -> Option { - self.events() - .filter(Sequence::up_to(resume_point)) - .collect() - } - - // Snapshot of this login, as of all events recorded in this history. - pub fn as_snapshot(&self) -> Option { - self.events().collect() - } -} - -// Events interface -impl History { - fn created(&self) -> Event { - Created { - instant: self.created, - login: self.login.clone(), - } - .into() - } - - pub fn events(&self) -> impl Iterator + use<> { - [self.created()].into_iter() - } -} diff --git a/src/login/id.rs b/src/login/id.rs deleted file mode 100644 index c46d697..0000000 --- a/src/login/id.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::id::Id as BaseId; - -// Stable identifier for a [Login]. Prefixed with `L`. -#[derive(Clone, Debug, Eq, PartialEq, sqlx::Type, serde::Serialize)] -#[sqlx(transparent)] -pub struct Id(BaseId); - -impl From for Id { - fn from(id: BaseId) -> Self { - Self(id) - } -} - -impl Id { - pub fn generate() -> Self { - BaseId::generate("L") - } -} - -impl std::fmt::Display for Id { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} diff --git a/src/login/mod.rs b/src/login/mod.rs deleted file mode 100644 index 006fa0c..0000000 --- a/src/login/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[cfg(test)] -pub mod app; -pub mod create; -pub mod event; -mod history; -mod id; -pub mod password; -pub mod repo; -mod routes; -mod snapshot; -mod validate; - -pub use self::{ - event::Event, history::History, id::Id, password::Password, routes::router, snapshot::Login, -}; diff --git a/src/login/password.rs b/src/login/password.rs deleted file mode 100644 index e1d164e..0000000 --- a/src/login/password.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::fmt; - -use argon2::Argon2; -use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; -use rand_core::OsRng; - -use crate::normalize::nfc; - -#[derive(sqlx::Type)] -#[sqlx(transparent)] -pub struct StoredHash(String); - -impl StoredHash { - pub fn verify(&self, password: &Password) -> Result { - let hash = PasswordHash::new(&self.0)?; - - match Argon2::default().verify_password(password.as_bytes(), &hash) { - // Successful authentication, not an error - Ok(()) => Ok(true), - // Unsuccessful authentication, also not an error - Err(password_hash::errors::Error::Password) => Ok(false), - // Password validation failed for some other reason, treat as an error - Err(err) => Err(err), - } - } -} - -impl fmt::Debug for StoredHash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("StoredHash").field(&"********").finish() - } -} - -#[derive(Clone, serde::Deserialize)] -#[serde(transparent)] -pub struct Password(nfc::String); - -impl Password { - pub fn hash(&self) -> Result { - let Self(password) = self; - let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - let hash = argon2 - .hash_password(password.as_bytes(), &salt)? - .to_string(); - Ok(StoredHash(hash)) - } - - fn as_bytes(&self) -> &[u8] { - let Self(value) = self; - value.as_bytes() - } -} - -impl fmt::Debug for Password { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Password").field(&"********").finish() - } -} - -impl From for Password { - fn from(password: String) -> Self { - Password(password.into()) - } -} diff --git a/src/login/repo.rs b/src/login/repo.rs deleted file mode 100644 index 128d6b8..0000000 --- a/src/login/repo.rs +++ /dev/null @@ -1,153 +0,0 @@ -use futures::stream::{StreamExt as _, TryStreamExt as _}; -use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; - -use crate::{ - clock::DateTime, - event::{Instant, Sequence}, - login::{History, Id, Login, password::StoredHash}, - name::{self, Name}, -}; - -pub trait Provider { - fn logins(&mut self) -> Logins; -} - -impl Provider for Transaction<'_, Sqlite> { - fn logins(&mut self) -> Logins { - Logins(self) - } -} - -pub struct Logins<'t>(&'t mut SqliteConnection); - -impl Logins<'_> { - pub async fn create( - &mut self, - name: &Name, - password_hash: &StoredHash, - created: &Instant, - ) -> Result { - let id = Id::generate(); - let display_name = name.display(); - let canonical_name = name.canonical(); - - sqlx::query!( - r#" - insert - into user (id, display_name, canonical_name, password_hash, created_sequence, created_at) - values ($1, $2, $3, $4, $5, $6) - "#, - id, - display_name, - canonical_name, - password_hash, - created.sequence, - created.at, - ) - .execute(&mut *self.0) - .await?; - - let login = History { - created: *created, - login: Login { - id, - name: name.clone(), - }, - }; - - Ok(login) - } - - pub async fn set_password( - &mut self, - login: &History, - to: &StoredHash, - ) -> Result<(), sqlx::Error> { - let login = login.id(); - - sqlx::query_scalar!( - r#" - update user - set password_hash = $1 - where id = $2 - returning id as "id: Id" - "#, - to, - login, - ) - .fetch_one(&mut *self.0) - .await?; - - Ok(()) - } - - pub async fn all(&mut self, resume_at: Sequence) -> Result, LoadError> { - let logins = sqlx::query!( - r#" - select - id as "id: Id", - display_name as "display_name: String", - canonical_name as "canonical_name: String", - created_sequence as "created_sequence: Sequence", - created_at as "created_at: DateTime" - from user - where created_sequence <= $1 - order by canonical_name - "#, - resume_at, - ) - .map(|row| { - Ok::<_, LoadError>(History { - login: Login { - id: row.id, - name: Name::new(row.display_name, row.canonical_name)?, - }, - created: Instant::new(row.created_at, row.created_sequence), - }) - }) - .fetch(&mut *self.0) - .map(|res| res?) - .try_collect() - .await?; - - Ok(logins) - } - - pub async fn replay(&mut self, resume_at: Sequence) -> Result, LoadError> { - let logins = sqlx::query!( - r#" - select - id as "id: Id", - display_name as "display_name: String", - canonical_name as "canonical_name: String", - created_sequence as "created_sequence: Sequence", - created_at as "created_at: DateTime" - from user - where created_sequence > $1 - "#, - resume_at, - ) - .map(|row| { - Ok::<_, name::Error>(History { - login: Login { - id: row.id, - name: Name::new(row.display_name, row.canonical_name)?, - }, - created: Instant::new(row.created_at, row.created_sequence), - }) - }) - .fetch(&mut *self.0) - .map(|res| Ok::<_, LoadError>(res??)) - .try_collect() - .await?; - - Ok(logins) - } -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub enum LoadError { - Database(#[from] sqlx::Error), - Name(#[from] name::Error), -} diff --git a/src/login/routes/login/mod.rs b/src/login/routes/login/mod.rs deleted file mode 100644 index 36b384e..0000000 --- a/src/login/routes/login/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod post; - -#[cfg(test)] -mod test; diff --git a/src/login/routes/login/post.rs b/src/login/routes/login/post.rs deleted file mode 100644 index 96da5c5..0000000 --- a/src/login/routes/login/post.rs +++ /dev/null @@ -1,52 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, - response::{IntoResponse, Response}, -}; - -use crate::{ - app::App, - clock::RequestedAt, - error::Internal, - login::{Login, Password}, - name::Name, - token::{app, extract::IdentityCookie}, -}; - -pub async fn handler( - State(app): State, - RequestedAt(now): RequestedAt, - identity: IdentityCookie, - Json(request): Json, -) -> Result<(IdentityCookie, Json), Error> { - let (login, secret) = app - .tokens() - .login(&request.name, &request.password, &now) - .await - .map_err(Error)?; - let identity = identity.set(secret); - Ok((identity, Json(login))) -} - -#[derive(serde::Deserialize)] -pub struct Request { - pub name: Name, - pub password: Password, -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(#[from] pub app::LoginError); - -impl IntoResponse for Error { - fn into_response(self) -> Response { - let Self(error) = self; - match error { - app::LoginError::Rejected => { - // not error::Unauthorized due to differing messaging - (StatusCode::UNAUTHORIZED, "invalid name or password").into_response() - } - other => Internal::from(other).into_response(), - } - } -} diff --git a/src/login/routes/login/test.rs b/src/login/routes/login/test.rs deleted file mode 100644 index 7399796..0000000 --- a/src/login/routes/login/test.rs +++ /dev/null @@ -1,128 +0,0 @@ -use axum::extract::{Json, State}; - -use super::post; -use crate::{test::fixtures, token::app}; - -#[tokio::test] -async fn correct_credentials() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let (name, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await; - - // Call the endpoint - - let identity = fixtures::cookie::not_logged_in(); - let logged_in_at = fixtures::now(); - let request = post::Request { - name: name.clone(), - password, - }; - let (identity, Json(response)) = - post::handler(State(app.clone()), logged_in_at, identity, Json(request)) - .await - .expect("logged in with valid credentials"); - - // Verify the return value's basic structure - - assert_eq!(name, response.name); - let secret = identity - .secret() - .expect("logged in with valid credentials issues an identity cookie"); - - // Verify the semantics - - let validated_at = fixtures::now(); - let (_, validated_login) = app - .tokens() - .validate(&secret, &validated_at) - .await - .expect("identity secret is valid"); - - assert_eq!(response, validated_login); -} - -#[tokio::test] -async fn invalid_name() { - // Set up the environment - - let app = fixtures::scratch_app().await; - - // Call the endpoint - - let identity = fixtures::cookie::not_logged_in(); - let logged_in_at = fixtures::now(); - let (name, password) = fixtures::login::propose(); - let request = post::Request { - name: name.clone(), - password, - }; - let post::Error(error) = - post::handler(State(app.clone()), logged_in_at, identity, Json(request)) - .await - .expect_err("logged in with an incorrect password fails"); - - // Verify the return value's basic structure - - assert!(matches!(error, app::LoginError::Rejected)); -} - -#[tokio::test] -async fn incorrect_password() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let login = fixtures::login::create(&app, &fixtures::now()).await; - - // Call the endpoint - - let logged_in_at = fixtures::now(); - let identity = fixtures::cookie::not_logged_in(); - let request = post::Request { - name: login.name, - password: fixtures::login::propose_password(), - }; - let post::Error(error) = - post::handler(State(app.clone()), logged_in_at, identity, Json(request)) - .await - .expect_err("logged in with an incorrect password"); - - // Verify the return value's basic structure - - assert!(matches!(error, app::LoginError::Rejected)); -} - -#[tokio::test] -async fn token_expires() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let (name, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await; - - // Call the endpoint - - let logged_in_at = fixtures::ancient(); - let identity = fixtures::cookie::not_logged_in(); - let request = post::Request { name, password }; - let (identity, _) = post::handler(State(app.clone()), logged_in_at, identity, Json(request)) - .await - .expect("logged in with valid credentials"); - let secret = identity.secret().expect("logged in with valid credentials"); - - // Verify the semantics - - let expired_at = fixtures::now(); - app.tokens() - .expire(&expired_at) - .await - .expect("expiring tokens never fails"); - - let verified_at = fixtures::now(); - let error = app - .tokens() - .validate(&secret, &verified_at) - .await - .expect_err("validating an expired token"); - - assert!(matches!(error, app::ValidateError::InvalidToken)); -} diff --git a/src/login/routes/logout/mod.rs b/src/login/routes/logout/mod.rs deleted file mode 100644 index 36b384e..0000000 --- a/src/login/routes/logout/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod post; - -#[cfg(test)] -mod test; diff --git a/src/login/routes/logout/post.rs b/src/login/routes/logout/post.rs deleted file mode 100644 index bb09b9f..0000000 --- a/src/login/routes/logout/post.rs +++ /dev/null @@ -1,47 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, - response::{IntoResponse, Response}, -}; - -use crate::{ - app::App, - clock::RequestedAt, - error::{Internal, Unauthorized}, - token::{app, extract::IdentityCookie}, -}; - -pub async fn handler( - State(app): State, - RequestedAt(now): RequestedAt, - identity: IdentityCookie, - Json(_): Json, -) -> Result<(IdentityCookie, StatusCode), Error> { - if let Some(secret) = identity.secret() { - let (token, _) = app.tokens().validate(&secret, &now).await?; - app.tokens().logout(&token).await?; - } - - let identity = identity.clear(); - Ok((identity, StatusCode::NO_CONTENT)) -} - -// This forces the only valid request to be `{}`, and not the infinite -// variation allowed when there's no body extractor. -#[derive(Default, serde::Deserialize)] -pub struct Request {} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(#[from] pub app::ValidateError); - -impl IntoResponse for Error { - fn into_response(self) -> Response { - let Self(error) = self; - #[allow(clippy::match_wildcard_for_single_variants)] - match error { - app::ValidateError::InvalidToken => Unauthorized.into_response(), - other => Internal::from(other).into_response(), - } - } -} diff --git a/src/login/routes/logout/test.rs b/src/login/routes/logout/test.rs deleted file mode 100644 index 775fa9f..0000000 --- a/src/login/routes/logout/test.rs +++ /dev/null @@ -1,79 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, -}; - -use super::post; -use crate::{test::fixtures, token::app}; - -#[tokio::test] -async fn successful() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let now = fixtures::now(); - let creds = fixtures::login::create_with_password(&app, &fixtures::now()).await; - let identity = fixtures::cookie::logged_in(&app, &creds, &now).await; - let secret = fixtures::cookie::secret(&identity); - - // Call the endpoint - - let (response_identity, response_status) = post::handler( - State(app.clone()), - fixtures::now(), - identity.clone(), - Json::default(), - ) - .await - .expect("logged out with a valid token"); - - // Verify the return value's basic structure - - assert!(response_identity.secret().is_none()); - assert_eq!(StatusCode::NO_CONTENT, response_status); - - // Verify the semantics - let error = app - .tokens() - .validate(&secret, &now) - .await - .expect_err("secret is invalid"); - assert!(matches!(error, app::ValidateError::InvalidToken)); -} - -#[tokio::test] -async fn no_identity() { - // Set up the environment - - let app = fixtures::scratch_app().await; - - // Call the endpoint - - let identity = fixtures::cookie::not_logged_in(); - let (identity, status) = post::handler(State(app), fixtures::now(), identity, Json::default()) - .await - .expect("logged out with no token succeeds"); - - // Verify the return value's basic structure - - assert!(identity.secret().is_none()); - assert_eq!(StatusCode::NO_CONTENT, status); -} - -#[tokio::test] -async fn invalid_token() { - // Set up the environment - - let app = fixtures::scratch_app().await; - - // Call the endpoint - - let identity = fixtures::cookie::fictitious(); - let post::Error(error) = post::handler(State(app), fixtures::now(), identity, Json::default()) - .await - .expect_err("logged out with an invalid token fails"); - - // Verify the return value's basic structure - - assert!(matches!(error, app::ValidateError::InvalidToken)); -} diff --git a/src/login/routes/mod.rs b/src/login/routes/mod.rs deleted file mode 100644 index ade96cb..0000000 --- a/src/login/routes/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -use axum::{Router, routing::post}; - -use crate::app::App; - -mod login; -mod logout; -mod password; - -pub fn router() -> Router { - Router::new() - .route("/api/password", post(password::post::handler)) - .route("/api/auth/login", post(login::post::handler)) - .route("/api/auth/logout", post(logout::post::handler)) -} diff --git a/src/login/routes/password/mod.rs b/src/login/routes/password/mod.rs deleted file mode 100644 index 36b384e..0000000 --- a/src/login/routes/password/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod post; - -#[cfg(test)] -mod test; diff --git a/src/login/routes/password/post.rs b/src/login/routes/password/post.rs deleted file mode 100644 index 4723754..0000000 --- a/src/login/routes/password/post.rs +++ /dev/null @@ -1,54 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, - response::{IntoResponse, Response}, -}; - -use crate::{ - app::App, - clock::RequestedAt, - error::Internal, - login::{Login, Password}, - token::{ - app, - extract::{Identity, IdentityCookie}, - }, -}; - -pub async fn handler( - State(app): State, - RequestedAt(now): RequestedAt, - identity: Identity, - cookie: IdentityCookie, - Json(request): Json, -) -> Result<(IdentityCookie, Json), Error> { - let (login, secret) = app - .tokens() - .change_password(&identity.login, &request.password, &request.to, &now) - .await - .map_err(Error)?; - let cookie = cookie.set(secret); - Ok((cookie, Json(login))) -} - -#[derive(serde::Deserialize)] -pub struct Request { - pub password: Password, - pub to: Password, -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(#[from] pub app::LoginError); - -impl IntoResponse for Error { - fn into_response(self) -> Response { - let Self(error) = self; - match error { - app::LoginError::Rejected => { - (StatusCode::BAD_REQUEST, "invalid name or password").into_response() - } - other => Internal::from(other).into_response(), - } - } -} diff --git a/src/login/routes/password/test.rs b/src/login/routes/password/test.rs deleted file mode 100644 index c1974bf..0000000 --- a/src/login/routes/password/test.rs +++ /dev/null @@ -1,68 +0,0 @@ -use axum::extract::{Json, State}; - -use super::post; -use crate::{ - test::fixtures, - token::app::{LoginError, ValidateError}, -}; - -#[tokio::test] -async fn password_change() { - // Set up the environment - let app = fixtures::scratch_app().await; - let creds = fixtures::login::create_with_password(&app, &fixtures::now()).await; - let cookie = fixtures::cookie::logged_in(&app, &creds, &fixtures::now()).await; - let identity = fixtures::identity::from_cookie(&app, &cookie, &fixtures::now()).await; - - // Call the endpoint - let (name, password) = creds; - let to = fixtures::login::propose_password(); - let request = post::Request { - password: password.clone(), - to: to.clone(), - }; - let (new_cookie, Json(response)) = post::handler( - State(app.clone()), - fixtures::now(), - identity.clone(), - cookie.clone(), - Json(request), - ) - .await - .expect("changing passwords succeeds"); - - // Verify that we have a new session - assert_ne!(cookie.secret(), new_cookie.secret()); - - // Verify that we're still ourselves - assert_eq!(identity.login, response); - - // Verify that our original token is no longer valid - let validate_err = app - .tokens() - .validate( - &cookie - .secret() - .expect("original identity cookie has a secret"), - &fixtures::now(), - ) - .await - .expect_err("validating the original identity secret should fail"); - assert!(matches!(validate_err, ValidateError::InvalidToken)); - - // Verify that our original password is no longer valid - let login_err = app - .tokens() - .login(&name, &password, &fixtures::now()) - .await - .expect_err("logging in with the original password should fail"); - assert!(matches!(login_err, LoginError::Rejected)); - - // Verify that our new password is valid - let (login, _) = app - .tokens() - .login(&name, &to, &fixtures::now()) - .await - .expect("logging in with the new password should succeed"); - assert_eq!(identity.login, login); -} diff --git a/src/login/snapshot.rs b/src/login/snapshot.rs deleted file mode 100644 index 5c5dce0..0000000 --- a/src/login/snapshot.rs +++ /dev/null @@ -1,51 +0,0 @@ -use super::{ - Id, - event::{Created, Event}, -}; -use crate::name::Name; - -// This also implements FromRequestParts (see `./extract.rs`). As a result, it -// can be used as an extractor for endpoints that want to require login, or for -// endpoints that need to behave differently depending on whether the client is -// or is not logged in. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -pub struct Login { - pub id: Id, - pub name: Name, - // The omission of the hashed password is deliberate, to minimize the - // chance that it ends up tangled up in debug output or in some other chunk - // of logic elsewhere. -} - -impl Login { - // Two reasons for this allow: - // - // 1. This is used to collect streams using a fold, below, which requires a type - // consistent with the fold, and - // 2. It's also consistent with the other history state machine types. - #[allow(clippy::unnecessary_wraps)] - fn apply(state: Option, event: Event) -> Option { - match (state, event) { - (None, Event::Created(event)) => Some(event.into()), - (state, event) => panic!("invalid message event {event:#?} for state {state:#?}"), - } - } -} - -impl FromIterator for Option { - fn from_iter>(events: I) -> Self { - events.into_iter().fold(None, Login::apply) - } -} - -impl From<&Created> for Login { - fn from(event: &Created) -> Self { - event.login.clone() - } -} - -impl From for Login { - fn from(event: Created) -> Self { - event.login - } -} diff --git a/src/login/validate.rs b/src/login/validate.rs deleted file mode 100644 index 0c97293..0000000 --- a/src/login/validate.rs +++ /dev/null @@ -1,23 +0,0 @@ -use unicode_segmentation::UnicodeSegmentation as _; - -use crate::name::Name; - -// Picked out of a hat. The power of two is not meaningful. -const NAME_TOO_LONG: usize = 64; - -pub fn name(name: &Name) -> bool { - let display = name.display(); - - [ - display.graphemes(true).count() < NAME_TOO_LONG, - display.chars().all(|ch| !ch.is_control()), - display.chars().next().is_some_and(|c| !c.is_whitespace()), - display.chars().last().is_some_and(|c| !c.is_whitespace()), - display - .chars() - .zip(display.chars().skip(1)) - .all(|(a, b)| !(a.is_whitespace() && b.is_whitespace())), - ] - .into_iter() - .all(|value| value) -} diff --git a/src/message/app.rs b/src/message/app.rs index 6f8f3d4..3c74628 100644 --- a/src/message/app.rs +++ b/src/message/app.rs @@ -8,8 +8,8 @@ use crate::{ clock::DateTime, db::NotFound as _, event::{Broadcaster, Event, Sequence, repo::Provider as _}, - login::Login, name, + user::User, }; pub struct Messages<'a> { @@ -25,7 +25,7 @@ impl<'a> Messages<'a> { pub async fn send( &self, channel: &channel::Id, - sender: &Login, + sender: &User, sent_at: &DateTime, body: &Body, ) -> Result { @@ -47,7 +47,7 @@ impl<'a> Messages<'a> { pub async fn delete( &self, - deleted_by: &Login, + deleted_by: &User, message: &Id, deleted_at: &DateTime, ) -> Result<(), DeleteError> { @@ -146,8 +146,8 @@ impl From for SendError { pub enum DeleteError { #[error("message {0} not found")] NotFound(Id), - #[error("login {} not the message's sender", .0.id)] - NotSender(Login), + #[error("user {} not the message's sender", .0.id)] + NotSender(User), #[error("message {0} deleted")] Deleted(Id), #[error(transparent)] diff --git a/src/message/repo.rs b/src/message/repo.rs index 8a0a72c..9a4f72f 100644 --- a/src/message/repo.rs +++ b/src/message/repo.rs @@ -5,7 +5,7 @@ use crate::{ channel, clock::DateTime, event::{Instant, Sequence}, - login::{self, Login}, + user::{self, User}, }; pub trait Provider { @@ -24,7 +24,7 @@ impl Messages<'_> { pub async fn create( &mut self, channel: &channel::History, - sender: &Login, + sender: &User, sent: &Instant, body: &Body, ) -> Result { @@ -39,7 +39,7 @@ impl Messages<'_> { returning id as "id: Id", channel as "channel: channel::Id", - sender as "sender: login::Id", + sender as "sender: user::Id", sent_at as "sent_at: DateTime", sent_sequence as "sent_sequence: Sequence", body as "body: Body" @@ -75,7 +75,7 @@ impl Messages<'_> { r#" select message.channel as "channel: channel::Id", - message.sender as "sender: login::Id", + message.sender as "sender: user::Id", id as "id: Id", message.body as "body: Body", message.sent_at as "sent_at: DateTime", @@ -112,7 +112,7 @@ impl Messages<'_> { r#" select message.channel as "channel: channel::Id", - message.sender as "sender: login::Id", + message.sender as "sender: user::Id", message.id as "id: Id", message.body as "body: Body", message.sent_at as "sent_at: DateTime", @@ -149,7 +149,7 @@ impl Messages<'_> { r#" select message.channel as "channel: channel::Id", - message.sender as "sender: login::Id", + message.sender as "sender: user::Id", id as "id: Id", message.body as "body: Body", message.sent_at as "sent_at: DateTime", @@ -254,7 +254,7 @@ impl Messages<'_> { select id as "id: Id", message.channel as "channel: channel::Id", - message.sender as "sender: login::Id", + message.sender as "sender: user::Id", message.sent_at as "sent_at: DateTime", message.sent_sequence as "sent_sequence: Sequence", message.body as "body: Body", @@ -291,7 +291,7 @@ impl Messages<'_> { select id as "id: Id", message.channel as "channel: channel::Id", - message.sender as "sender: login::Id", + message.sender as "sender: user::Id", message.sent_at as "sent_at: DateTime", message.sent_sequence as "sent_sequence: Sequence", message.body as "body: Body", diff --git a/src/message/routes/message/mod.rs b/src/message/routes/message/mod.rs index e92f556..4abd445 100644 --- a/src/message/routes/message/mod.rs +++ b/src/message/routes/message/mod.rs @@ -23,7 +23,7 @@ pub mod delete { identity: Identity, ) -> Result { app.messages() - .delete(&identity.login, &message, &deleted_at) + .delete(&identity.user, &message, &deleted_at) .await?; Ok(Response { id: message }) diff --git a/src/message/routes/message/test.rs b/src/message/routes/message/test.rs index 5178ab5..1abd91c 100644 --- a/src/message/routes/message/test.rs +++ b/src/message/routes/message/test.rs @@ -10,7 +10,7 @@ pub async fn delete_message() { let app = fixtures::scratch_app().await; let sender = fixtures::identity::create(&app, &fixtures::now()).await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; - let message = fixtures::message::send(&app, &channel, &sender.login, &fixtures::now()).await; + let message = fixtures::message::send(&app, &channel, &sender.user, &fixtures::now()).await; // Send the request @@ -179,6 +179,6 @@ pub async fn delete_not_sender() { // Verify the response assert!( - matches!(error, app::DeleteError::NotSender(error_sender) if deleter.login == error_sender) + matches!(error, app::DeleteError::NotSender(error_sender) if deleter.user == error_sender) ); } diff --git a/src/message/snapshot.rs b/src/message/snapshot.rs index d924ea1..ac067f7 100644 --- a/src/message/snapshot.rs +++ b/src/message/snapshot.rs @@ -2,14 +2,14 @@ use super::{ Body, Id, event::{Event, Sent}, }; -use crate::{channel, clock::DateTime, event::Instant, login}; +use crate::{channel, clock::DateTime, event::Instant, user}; #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct Message { #[serde(flatten)] pub sent: Instant, pub channel: channel::Id, - pub sender: login::Id, + pub sender: user::Id, pub id: Id, pub body: Body, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/setup/app.rs b/src/setup/app.rs index 9553f40..9f31c01 100644 --- a/src/setup/app.rs +++ b/src/setup/app.rs @@ -4,12 +4,12 @@ use super::repo::Provider as _; use crate::{ clock::DateTime, event::Broadcaster, - login::{ - Login, Password, - create::{self, Create}, - }, name::Name, token::{Secret, repo::Provider as _}, + user::{ + Password, User, + create::{self, Create}, + }, }; pub struct Setup<'a> { @@ -27,7 +27,7 @@ impl<'a> Setup<'a> { name: &Name, password: &Password, created_at: &DateTime, - ) -> Result<(Login, Secret), Error> { + ) -> Result<(User, Secret), Error> { let create = Create::begin(name, password, created_at); let validated = create.validate()?; @@ -38,7 +38,7 @@ impl<'a> Setup<'a> { } else { validated.store(&mut tx).await? }; - let secret = tx.tokens().issue(stored.login(), created_at).await?; + let secret = tx.tokens().issue(stored.user(), created_at).await?; tx.commit().await?; let login = stored.publish(self.events); @@ -59,7 +59,7 @@ impl<'a> Setup<'a> { pub enum Error { #[error("initial setup previously completed")] SetupCompleted, - #[error("invalid login name: {0}")] + #[error("invalid user name: {0}")] InvalidName(Name), #[error(transparent)] Database(#[from] sqlx::Error), diff --git a/src/setup/routes/post.rs b/src/setup/routes/post.rs index 2a46b04..9c6b7a6 100644 --- a/src/setup/routes/post.rs +++ b/src/setup/routes/post.rs @@ -8,10 +8,10 @@ use crate::{ app::App, clock::RequestedAt, error::Internal, - login::{Login, Password}, name::Name, setup::app, token::extract::IdentityCookie, + user::{Password, User}, }; pub async fn handler( @@ -19,7 +19,7 @@ pub async fn handler( RequestedAt(setup_at): RequestedAt, identity: IdentityCookie, Json(request): Json, -) -> Result<(IdentityCookie, Json), Error> { +) -> Result<(IdentityCookie, Json), Error> { let (login, secret) = app .setup() .initial(&request.name, &request.password, &setup_at) diff --git a/src/test/fixtures/cookie.rs b/src/test/fixtures/cookie.rs index fcb379f..bba53b8 100644 --- a/src/test/fixtures/cookie.rs +++ b/src/test/fixtures/cookie.rs @@ -3,9 +3,9 @@ use uuid::Uuid; use crate::{ app::App, clock::RequestedAt, - login::Password, name::Name, token::{Secret, extract::IdentityCookie}, + user::Password, }; pub fn not_logged_in() -> IdentityCookie { diff --git a/src/test/fixtures/event.rs b/src/test/fixtures/event.rs index e11f6ee..c6e5337 100644 --- a/src/test/fixtures/event.rs +++ b/src/test/fixtures/event.rs @@ -68,8 +68,8 @@ pub mod message { pub mod login { use std::future::{self, Ready}; - pub use crate::login::Event; - use crate::login::event; + pub use crate::user::Event; + use crate::user::event; pub fn created(event: Event) -> Ready> { future::ready(match event { diff --git a/src/test/fixtures/identity.rs b/src/test/fixtures/identity.rs index ffc44c6..7611066 100644 --- a/src/test/fixtures/identity.rs +++ b/src/test/fixtures/identity.rs @@ -1,13 +1,13 @@ use crate::{ app::App, clock::RequestedAt, - login::Password, name::Name, test::fixtures, token::{ self, extract::{Identity, IdentityCookie}, }, + user::Password, }; pub async fn create(app: &App, created_at: &RequestedAt) -> Identity { @@ -27,7 +27,7 @@ pub async fn from_cookie( .await .expect("always validates newly-issued secret"); - Identity { token, login } + Identity { token, user: login } } pub async fn logged_in( @@ -43,5 +43,5 @@ pub fn fictitious() -> Identity { let token = token::Id::generate(); let login = fixtures::login::fictitious(); - Identity { token, login } + Identity { token, user: login } } diff --git a/src/test/fixtures/invite.rs b/src/test/fixtures/invite.rs index 654d1b4..7a41eb6 100644 --- a/src/test/fixtures/invite.rs +++ b/src/test/fixtures/invite.rs @@ -2,10 +2,10 @@ use crate::{ app::App, clock::DateTime, invite::{self, Invite}, - login::Login, + user::User, }; -pub async fn issue(app: &App, issuer: &Login, issued_at: &DateTime) -> Invite { +pub async fn issue(app: &App, issuer: &User, issued_at: &DateTime) -> Invite { app.invites() .issue(issuer, issued_at) .await diff --git a/src/test/fixtures/login.rs b/src/test/fixtures/login.rs index 86e3e39..e668c95 100644 --- a/src/test/fixtures/login.rs +++ b/src/test/fixtures/login.rs @@ -4,14 +4,14 @@ use uuid::Uuid; use crate::{ app::App, clock::RequestedAt, - login::{self, Login, Password}, name::Name, + user::{self, Password, User}, }; pub async fn create_with_password(app: &App, created_at: &RequestedAt) -> (Name, Password) { let (name, password) = propose(); let login = app - .logins() + .users() .create(&name, &password, created_at) .await .expect("should always succeed if the login is actually new"); @@ -19,17 +19,17 @@ pub async fn create_with_password(app: &App, created_at: &RequestedAt) -> (Name, (login.name, password) } -pub async fn create(app: &App, created_at: &RequestedAt) -> Login { +pub async fn create(app: &App, created_at: &RequestedAt) -> User { let (name, password) = propose(); - app.logins() + app.users() .create(&name, &password, created_at) .await .expect("should always succeed if the login is actually new") } -pub fn fictitious() -> Login { - Login { - id: login::Id::generate(), +pub fn fictitious() -> User { + User { + id: user::Id::generate(), name: propose_name(), } } diff --git a/src/test/fixtures/message.rs b/src/test/fixtures/message.rs index d3b4719..2254915 100644 --- a/src/test/fixtures/message.rs +++ b/src/test/fixtures/message.rs @@ -4,11 +4,11 @@ use crate::{ app::App, channel::Channel, clock::RequestedAt, - login::Login, message::{self, Body, Message}, + user::User, }; -pub async fn send(app: &App, channel: &Channel, sender: &Login, sent_at: &RequestedAt) -> Message { +pub async fn send(app: &App, channel: &Channel, sender: &User, sent_at: &RequestedAt) -> Message { let body = propose(); app.messages() diff --git a/src/token/app.rs b/src/token/app.rs index 3f054ff..211df81 100644 --- a/src/token/app.rs +++ b/src/token/app.rs @@ -12,8 +12,8 @@ use super::{ use crate::{ clock::DateTime, db::NotFound as _, - login::{Login, Password, repo::Provider as _}, name::{self, Name}, + user::{Password, User, repo::Provider as _}, }; pub struct Tokens<'a> { @@ -31,7 +31,7 @@ impl<'a> Tokens<'a> { name: &Name, password: &Password, login_at: &DateTime, - ) -> Result<(Login, Secret), LoginError> { + ) -> Result<(User, Secret), LoginError> { let mut tx = self.db.begin().await?; let (login, stored_hash) = tx .auth() @@ -62,11 +62,11 @@ impl<'a> Tokens<'a> { pub async fn change_password( &self, - login: &Login, + login: &User, password: &Password, to: &Password, changed_at: &DateTime, - ) -> Result<(Login, Secret), LoginError> { + ) -> Result<(User, Secret), LoginError> { let mut tx = self.db.begin().await?; let (login, stored_hash) = tx .auth() @@ -90,7 +90,7 @@ impl<'a> Tokens<'a> { let mut tx = self.db.begin().await?; let tokens = tx.tokens().revoke_all(&login).await?; - tx.logins().set_password(&login, &to_hash).await?; + tx.users().set_password(&login, &to_hash).await?; let secret = tx.tokens().issue(&login, changed_at).await?; tx.commit().await?; @@ -105,7 +105,7 @@ impl<'a> Tokens<'a> { &self, secret: &Secret, used_at: &DateTime, - ) -> Result<(Id, Login), ValidateError> { + ) -> Result<(Id, User), ValidateError> { let mut tx = self.db.begin().await?; let (token, login) = tx .tokens() @@ -226,7 +226,7 @@ impl From for LoginError { pub enum ValidateError { #[error("invalid token")] InvalidToken, - #[error("login deleted")] + #[error("user deleted")] LoginDeleted, #[error(transparent)] Database(#[from] sqlx::Error), diff --git a/src/token/extract/identity.rs b/src/token/extract/identity.rs index acfd7ae..d1c0334 100644 --- a/src/token/extract/identity.rs +++ b/src/token/extract/identity.rs @@ -10,14 +10,14 @@ use crate::{ app::App, clock::RequestedAt, error::{Internal, Unauthorized}, - login::Login, token::{self, app::ValidateError}, + user::User, }; #[derive(Clone, Debug)] pub struct Identity { pub token: token::Id, - pub login: Login, + pub user: User, } impl FromRequestParts for Identity { @@ -31,7 +31,7 @@ impl FromRequestParts for Identity { let app = State::::from_request_parts(parts, state).await?; match app.tokens().validate(&secret, &used_at).await { - Ok((token, login)) => Ok(Identity { token, login }), + Ok((token, user)) => Ok(Identity { token, user }), Err(ValidateError::InvalidToken) => Err(LoginError::Unauthorized), Err(other) => Err(other.into()), } diff --git a/src/token/repo/auth.rs b/src/token/repo/auth.rs index 8900704..a1f4aad 100644 --- a/src/token/repo/auth.rs +++ b/src/token/repo/auth.rs @@ -4,8 +4,8 @@ use crate::{ clock::DateTime, db::NotFound, event::{Instant, Sequence}, - login::{self, History, Login, password::StoredHash}, name::{self, Name}, + user::{self, History, User, password::StoredHash}, }; pub trait Provider { @@ -26,7 +26,7 @@ impl Auth<'_> { let row = sqlx::query!( r#" select - id as "id: login::Id", + id as "id: user::Id", display_name as "display_name: String", canonical_name as "canonical_name: String", created_sequence as "created_sequence: Sequence", @@ -41,7 +41,7 @@ impl Auth<'_> { .await?; let login = History { - login: Login { + user: User { id: row.id, name: Name::new(row.display_name, row.canonical_name)?, }, @@ -51,11 +51,11 @@ impl Auth<'_> { Ok((login, row.password_hash)) } - pub async fn for_login(&mut self, login: &Login) -> Result<(History, StoredHash), LoadError> { + pub async fn for_login(&mut self, login: &User) -> Result<(History, StoredHash), LoadError> { let row = sqlx::query!( r#" select - id as "id: login::Id", + id as "id: user::Id", display_name as "display_name: String", canonical_name as "canonical_name: String", created_sequence as "created_sequence: Sequence", @@ -70,7 +70,7 @@ impl Auth<'_> { .await?; let login = History { - login: Login { + user: User { id: row.id, name: Name::new(row.display_name, row.canonical_name)?, }, diff --git a/src/token/repo/token.rs b/src/token/repo/token.rs index 3428030..145ba2d 100644 --- a/src/token/repo/token.rs +++ b/src/token/repo/token.rs @@ -5,9 +5,9 @@ use crate::{ clock::DateTime, db::NotFound, event::{Instant, Sequence}, - login::{self, History, Login}, name::{self, Name}, token::{Id, Secret}, + user::{self, History, User}, }; pub trait Provider { @@ -85,7 +85,7 @@ impl Tokens<'_> { } // Revoke tokens for a login - pub async fn revoke_all(&mut self, login: &login::History) -> Result, sqlx::Error> { + pub async fn revoke_all(&mut self, login: &user::History) -> Result, sqlx::Error> { let login = login.id(); let tokens = sqlx::query_scalar!( r#" @@ -139,7 +139,7 @@ impl Tokens<'_> { where secret = $2 returning id as "token: Id", - user as "user: login::Id" + user as "user: user::Id" "#, used_at, secret, @@ -151,7 +151,7 @@ impl Tokens<'_> { let login = sqlx::query!( r#" select - id as "id: login::Id", + id as "id: user::Id", display_name as "display_name: String", canonical_name as "canonical_name: String", created_sequence as "created_sequence: Sequence", @@ -163,7 +163,7 @@ impl Tokens<'_> { ) .map(|row| { Ok::<_, name::Error>(History { - login: Login { + user: User { id: row.id, name: Name::new(row.display_name, row.canonical_name)?, }, diff --git a/src/user/app.rs b/src/user/app.rs new file mode 100644 index 0000000..2ab356f --- /dev/null +++ b/src/user/app.rs @@ -0,0 +1,56 @@ +use sqlx::sqlite::SqlitePool; + +use super::{ + Password, User, + create::{self, Create}, +}; +use crate::{clock::DateTime, event::Broadcaster, name::Name}; + +pub struct Users<'a> { + db: &'a SqlitePool, + events: &'a Broadcaster, +} + +impl<'a> Users<'a> { + pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { + Self { db, events } + } + + pub async fn create( + &self, + name: &Name, + password: &Password, + created_at: &DateTime, + ) -> Result { + let create = Create::begin(name, password, created_at); + let validated = create.validate()?; + + let mut tx = self.db.begin().await?; + let stored = validated.store(&mut tx).await?; + tx.commit().await?; + + let user = stored.publish(self.events); + + Ok(user.as_created()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum CreateError { + #[error("invalid user name: {0}")] + InvalidName(Name), + #[error(transparent)] + PasswordHash(#[from] password_hash::Error), + #[error(transparent)] + Database(#[from] sqlx::Error), +} + +#[cfg(test)] +impl From for CreateError { + fn from(error: create::Error) -> Self { + match error { + create::Error::InvalidName(name) => Self::InvalidName(name), + create::Error::PasswordHash(error) => Self::PasswordHash(error), + } + } +} diff --git a/src/user/create.rs b/src/user/create.rs new file mode 100644 index 0000000..da94685 --- /dev/null +++ b/src/user/create.rs @@ -0,0 +1,95 @@ +use sqlx::{Transaction, sqlite::Sqlite}; + +use super::{History, Password, password::StoredHash, repo::Provider as _, validate}; +use crate::{ + clock::DateTime, + event::{Broadcaster, Event, repo::Provider as _}, + name::Name, +}; + +#[must_use = "dropping a user creation attempt is likely a mistake"] +pub struct Create<'a> { + name: &'a Name, + password: &'a Password, + created_at: &'a DateTime, +} + +impl<'a> Create<'a> { + pub fn begin(name: &'a Name, password: &'a Password, created_at: &'a DateTime) -> Self { + Self { + name, + password, + created_at, + } + } + + pub fn validate(self) -> Result, Error> { + let Self { + name, + password, + created_at, + } = self; + + if !validate::name(name) { + return Err(Error::InvalidName(name.clone())); + } + + let password_hash = password.hash()?; + + Ok(Validated { + name, + password_hash, + created_at, + }) + } +} + +#[must_use = "dropping a user creation attempt is likely a mistake"] +pub struct Validated<'a> { + name: &'a Name, + password_hash: StoredHash, + created_at: &'a DateTime, +} + +impl Validated<'_> { + pub async fn store(self, tx: &mut Transaction<'_, Sqlite>) -> Result { + let Self { + name, + password_hash, + created_at, + } = self; + + let created = tx.sequence().next(created_at).await?; + let user = tx.users().create(name, &password_hash, &created).await?; + + Ok(Stored { user }) + } +} + +#[must_use = "dropping a user creation attempt is likely a mistake"] +pub struct Stored { + user: History, +} + +impl Stored { + #[must_use = "dropping a user creation attempt is likely a mistake"] + pub fn publish(self, events: &Broadcaster) -> History { + let Self { user } = self; + + events.broadcast(user.events().map(Event::from).collect::>()); + + user + } + + pub fn user(&self) -> &History { + &self.user + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("invalid user name: {0}")] + InvalidName(Name), + #[error(transparent)] + PasswordHash(#[from] password_hash::Error), +} diff --git a/src/user/event.rs b/src/user/event.rs new file mode 100644 index 0000000..a748112 --- /dev/null +++ b/src/user/event.rs @@ -0,0 +1,36 @@ +use super::snapshot::User; +use crate::event::{Instant, Sequenced}; + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +#[serde(tag = "event", rename_all = "snake_case")] +pub enum Event { + Created(Created), +} + +impl Sequenced for Event { + fn instant(&self) -> Instant { + match self { + Self::Created(created) => created.instant(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +pub struct Created { + #[serde(flatten)] + pub instant: Instant, + #[serde(flatten)] + pub user: User, +} + +impl Sequenced for Created { + fn instant(&self) -> Instant { + self.instant + } +} + +impl From for Event { + fn from(event: Created) -> Self { + Self::Created(event) + } +} diff --git a/src/user/history.rs b/src/user/history.rs new file mode 100644 index 0000000..ae7a561 --- /dev/null +++ b/src/user/history.rs @@ -0,0 +1,52 @@ +use super::{ + Id, User, + event::{Created, Event}, +}; +use crate::event::{Instant, Sequence}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct History { + pub user: User, + pub created: Instant, +} + +// State interface +impl History { + pub fn id(&self) -> &Id { + &self.user.id + } + + // Snapshot of this user as it was when created. (Note to the future: it's okay + // if this returns a redacted or modified version of the user. If we implement + // renames by redacting the original name, then this should return the edited + // user, not the original, even if that's not how it was "as created.") + pub fn as_created(&self) -> User { + self.user.clone() + } + + pub fn as_of(&self, resume_point: Sequence) -> Option { + self.events() + .filter(Sequence::up_to(resume_point)) + .collect() + } + + // Snapshot of this user, as of all events recorded in this history. + pub fn as_snapshot(&self) -> Option { + self.events().collect() + } +} + +// Events interface +impl History { + fn created(&self) -> Event { + Created { + instant: self.created, + user: self.user.clone(), + } + .into() + } + + pub fn events(&self) -> impl Iterator + use<> { + [self.created()].into_iter() + } +} diff --git a/src/user/id.rs b/src/user/id.rs new file mode 100644 index 0000000..9455deb --- /dev/null +++ b/src/user/id.rs @@ -0,0 +1,24 @@ +use crate::id::Id as BaseId; + +// Stable identifier for a User. Prefixed with `L`. +#[derive(Clone, Debug, Eq, PartialEq, sqlx::Type, serde::Serialize)] +#[sqlx(transparent)] +pub struct Id(BaseId); + +impl From for Id { + fn from(id: BaseId) -> Self { + Self(id) + } +} + +impl Id { + pub fn generate() -> Self { + BaseId::generate("L") + } +} + +impl std::fmt::Display for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} diff --git a/src/user/mod.rs b/src/user/mod.rs new file mode 100644 index 0000000..f4c66ab --- /dev/null +++ b/src/user/mod.rs @@ -0,0 +1,15 @@ +#[cfg(test)] +pub mod app; +pub mod create; +pub mod event; +mod history; +mod id; +pub mod password; +pub mod repo; +mod routes; +mod snapshot; +mod validate; + +pub use self::{ + event::Event, history::History, id::Id, password::Password, routes::router, snapshot::User, +}; diff --git a/src/user/password.rs b/src/user/password.rs new file mode 100644 index 0000000..e1d164e --- /dev/null +++ b/src/user/password.rs @@ -0,0 +1,65 @@ +use std::fmt; + +use argon2::Argon2; +use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; +use rand_core::OsRng; + +use crate::normalize::nfc; + +#[derive(sqlx::Type)] +#[sqlx(transparent)] +pub struct StoredHash(String); + +impl StoredHash { + pub fn verify(&self, password: &Password) -> Result { + let hash = PasswordHash::new(&self.0)?; + + match Argon2::default().verify_password(password.as_bytes(), &hash) { + // Successful authentication, not an error + Ok(()) => Ok(true), + // Unsuccessful authentication, also not an error + Err(password_hash::errors::Error::Password) => Ok(false), + // Password validation failed for some other reason, treat as an error + Err(err) => Err(err), + } + } +} + +impl fmt::Debug for StoredHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("StoredHash").field(&"********").finish() + } +} + +#[derive(Clone, serde::Deserialize)] +#[serde(transparent)] +pub struct Password(nfc::String); + +impl Password { + pub fn hash(&self) -> Result { + let Self(password) = self; + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let hash = argon2 + .hash_password(password.as_bytes(), &salt)? + .to_string(); + Ok(StoredHash(hash)) + } + + fn as_bytes(&self) -> &[u8] { + let Self(value) = self; + value.as_bytes() + } +} + +impl fmt::Debug for Password { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Password").field(&"********").finish() + } +} + +impl From for Password { + fn from(password: String) -> Self { + Password(password.into()) + } +} diff --git a/src/user/repo.rs b/src/user/repo.rs new file mode 100644 index 0000000..c02d50f --- /dev/null +++ b/src/user/repo.rs @@ -0,0 +1,153 @@ +use futures::stream::{StreamExt as _, TryStreamExt as _}; +use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; + +use crate::{ + clock::DateTime, + event::{Instant, Sequence}, + name::{self, Name}, + user::{History, Id, User, password::StoredHash}, +}; + +pub trait Provider { + fn users(&mut self) -> Users; +} + +impl Provider for Transaction<'_, Sqlite> { + fn users(&mut self) -> Users { + Users(self) + } +} + +pub struct Users<'t>(&'t mut SqliteConnection); + +impl Users<'_> { + pub async fn create( + &mut self, + name: &Name, + password_hash: &StoredHash, + created: &Instant, + ) -> Result { + let id = Id::generate(); + let display_name = name.display(); + let canonical_name = name.canonical(); + + sqlx::query!( + r#" + insert + into user (id, display_name, canonical_name, password_hash, created_sequence, created_at) + values ($1, $2, $3, $4, $5, $6) + "#, + id, + display_name, + canonical_name, + password_hash, + created.sequence, + created.at, + ) + .execute(&mut *self.0) + .await?; + + let user = History { + created: *created, + user: User { + id, + name: name.clone(), + }, + }; + + Ok(user) + } + + pub async fn set_password( + &mut self, + login: &History, + to: &StoredHash, + ) -> Result<(), sqlx::Error> { + let login = login.id(); + + sqlx::query_scalar!( + r#" + update user + set password_hash = $1 + where id = $2 + returning id as "id: Id" + "#, + to, + login, + ) + .fetch_one(&mut *self.0) + .await?; + + Ok(()) + } + + pub async fn all(&mut self, resume_at: Sequence) -> Result, LoadError> { + let logins = sqlx::query!( + r#" + select + id as "id: Id", + display_name as "display_name: String", + canonical_name as "canonical_name: String", + created_sequence as "created_sequence: Sequence", + created_at as "created_at: DateTime" + from user + where created_sequence <= $1 + order by canonical_name + "#, + resume_at, + ) + .map(|row| { + Ok::<_, LoadError>(History { + user: User { + id: row.id, + name: Name::new(row.display_name, row.canonical_name)?, + }, + created: Instant::new(row.created_at, row.created_sequence), + }) + }) + .fetch(&mut *self.0) + .map(|res| res?) + .try_collect() + .await?; + + Ok(logins) + } + + pub async fn replay(&mut self, resume_at: Sequence) -> Result, LoadError> { + let logins = sqlx::query!( + r#" + select + id as "id: Id", + display_name as "display_name: String", + canonical_name as "canonical_name: String", + created_sequence as "created_sequence: Sequence", + created_at as "created_at: DateTime" + from user + where created_sequence > $1 + "#, + resume_at, + ) + .map(|row| { + Ok::<_, name::Error>(History { + user: User { + id: row.id, + name: Name::new(row.display_name, row.canonical_name)?, + }, + created: Instant::new(row.created_at, row.created_sequence), + }) + }) + .fetch(&mut *self.0) + .map(|res| Ok::<_, LoadError>(res??)) + .try_collect() + .await?; + + Ok(logins) + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub enum LoadError { + Database(#[from] sqlx::Error), + Name(#[from] name::Error), +} diff --git a/src/user/routes/login/mod.rs b/src/user/routes/login/mod.rs new file mode 100644 index 0000000..36b384e --- /dev/null +++ b/src/user/routes/login/mod.rs @@ -0,0 +1,4 @@ +pub mod post; + +#[cfg(test)] +mod test; diff --git a/src/user/routes/login/post.rs b/src/user/routes/login/post.rs new file mode 100644 index 0000000..39f9eea --- /dev/null +++ b/src/user/routes/login/post.rs @@ -0,0 +1,52 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use crate::{ + app::App, + clock::RequestedAt, + error::Internal, + name::Name, + token::{app, extract::IdentityCookie}, + user::{Password, User}, +}; + +pub async fn handler( + State(app): State, + RequestedAt(now): RequestedAt, + identity: IdentityCookie, + Json(request): Json, +) -> Result<(IdentityCookie, Json), Error> { + let (user, secret) = app + .tokens() + .login(&request.name, &request.password, &now) + .await + .map_err(Error)?; + let identity = identity.set(secret); + Ok((identity, Json(user))) +} + +#[derive(serde::Deserialize)] +pub struct Request { + pub name: Name, + pub password: Password, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] pub app::LoginError); + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let Self(error) = self; + match error { + app::LoginError::Rejected => { + // not error::Unauthorized due to differing messaging + (StatusCode::UNAUTHORIZED, "invalid name or password").into_response() + } + other => Internal::from(other).into_response(), + } + } +} diff --git a/src/user/routes/login/test.rs b/src/user/routes/login/test.rs new file mode 100644 index 0000000..7399796 --- /dev/null +++ b/src/user/routes/login/test.rs @@ -0,0 +1,128 @@ +use axum::extract::{Json, State}; + +use super::post; +use crate::{test::fixtures, token::app}; + +#[tokio::test] +async fn correct_credentials() { + // Set up the environment + + let app = fixtures::scratch_app().await; + let (name, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await; + + // Call the endpoint + + let identity = fixtures::cookie::not_logged_in(); + let logged_in_at = fixtures::now(); + let request = post::Request { + name: name.clone(), + password, + }; + let (identity, Json(response)) = + post::handler(State(app.clone()), logged_in_at, identity, Json(request)) + .await + .expect("logged in with valid credentials"); + + // Verify the return value's basic structure + + assert_eq!(name, response.name); + let secret = identity + .secret() + .expect("logged in with valid credentials issues an identity cookie"); + + // Verify the semantics + + let validated_at = fixtures::now(); + let (_, validated_login) = app + .tokens() + .validate(&secret, &validated_at) + .await + .expect("identity secret is valid"); + + assert_eq!(response, validated_login); +} + +#[tokio::test] +async fn invalid_name() { + // Set up the environment + + let app = fixtures::scratch_app().await; + + // Call the endpoint + + let identity = fixtures::cookie::not_logged_in(); + let logged_in_at = fixtures::now(); + let (name, password) = fixtures::login::propose(); + let request = post::Request { + name: name.clone(), + password, + }; + let post::Error(error) = + post::handler(State(app.clone()), logged_in_at, identity, Json(request)) + .await + .expect_err("logged in with an incorrect password fails"); + + // Verify the return value's basic structure + + assert!(matches!(error, app::LoginError::Rejected)); +} + +#[tokio::test] +async fn incorrect_password() { + // Set up the environment + + let app = fixtures::scratch_app().await; + let login = fixtures::login::create(&app, &fixtures::now()).await; + + // Call the endpoint + + let logged_in_at = fixtures::now(); + let identity = fixtures::cookie::not_logged_in(); + let request = post::Request { + name: login.name, + password: fixtures::login::propose_password(), + }; + let post::Error(error) = + post::handler(State(app.clone()), logged_in_at, identity, Json(request)) + .await + .expect_err("logged in with an incorrect password"); + + // Verify the return value's basic structure + + assert!(matches!(error, app::LoginError::Rejected)); +} + +#[tokio::test] +async fn token_expires() { + // Set up the environment + + let app = fixtures::scratch_app().await; + let (name, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await; + + // Call the endpoint + + let logged_in_at = fixtures::ancient(); + let identity = fixtures::cookie::not_logged_in(); + let request = post::Request { name, password }; + let (identity, _) = post::handler(State(app.clone()), logged_in_at, identity, Json(request)) + .await + .expect("logged in with valid credentials"); + let secret = identity.secret().expect("logged in with valid credentials"); + + // Verify the semantics + + let expired_at = fixtures::now(); + app.tokens() + .expire(&expired_at) + .await + .expect("expiring tokens never fails"); + + let verified_at = fixtures::now(); + let error = app + .tokens() + .validate(&secret, &verified_at) + .await + .expect_err("validating an expired token"); + + assert!(matches!(error, app::ValidateError::InvalidToken)); +} diff --git a/src/user/routes/logout/mod.rs b/src/user/routes/logout/mod.rs new file mode 100644 index 0000000..36b384e --- /dev/null +++ b/src/user/routes/logout/mod.rs @@ -0,0 +1,4 @@ +pub mod post; + +#[cfg(test)] +mod test; diff --git a/src/user/routes/logout/post.rs b/src/user/routes/logout/post.rs new file mode 100644 index 0000000..bb09b9f --- /dev/null +++ b/src/user/routes/logout/post.rs @@ -0,0 +1,47 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use crate::{ + app::App, + clock::RequestedAt, + error::{Internal, Unauthorized}, + token::{app, extract::IdentityCookie}, +}; + +pub async fn handler( + State(app): State, + RequestedAt(now): RequestedAt, + identity: IdentityCookie, + Json(_): Json, +) -> Result<(IdentityCookie, StatusCode), Error> { + if let Some(secret) = identity.secret() { + let (token, _) = app.tokens().validate(&secret, &now).await?; + app.tokens().logout(&token).await?; + } + + let identity = identity.clear(); + Ok((identity, StatusCode::NO_CONTENT)) +} + +// This forces the only valid request to be `{}`, and not the infinite +// variation allowed when there's no body extractor. +#[derive(Default, serde::Deserialize)] +pub struct Request {} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] pub app::ValidateError); + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let Self(error) = self; + #[allow(clippy::match_wildcard_for_single_variants)] + match error { + app::ValidateError::InvalidToken => Unauthorized.into_response(), + other => Internal::from(other).into_response(), + } + } +} diff --git a/src/user/routes/logout/test.rs b/src/user/routes/logout/test.rs new file mode 100644 index 0000000..775fa9f --- /dev/null +++ b/src/user/routes/logout/test.rs @@ -0,0 +1,79 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, +}; + +use super::post; +use crate::{test::fixtures, token::app}; + +#[tokio::test] +async fn successful() { + // Set up the environment + + let app = fixtures::scratch_app().await; + let now = fixtures::now(); + let creds = fixtures::login::create_with_password(&app, &fixtures::now()).await; + let identity = fixtures::cookie::logged_in(&app, &creds, &now).await; + let secret = fixtures::cookie::secret(&identity); + + // Call the endpoint + + let (response_identity, response_status) = post::handler( + State(app.clone()), + fixtures::now(), + identity.clone(), + Json::default(), + ) + .await + .expect("logged out with a valid token"); + + // Verify the return value's basic structure + + assert!(response_identity.secret().is_none()); + assert_eq!(StatusCode::NO_CONTENT, response_status); + + // Verify the semantics + let error = app + .tokens() + .validate(&secret, &now) + .await + .expect_err("secret is invalid"); + assert!(matches!(error, app::ValidateError::InvalidToken)); +} + +#[tokio::test] +async fn no_identity() { + // Set up the environment + + let app = fixtures::scratch_app().await; + + // Call the endpoint + + let identity = fixtures::cookie::not_logged_in(); + let (identity, status) = post::handler(State(app), fixtures::now(), identity, Json::default()) + .await + .expect("logged out with no token succeeds"); + + // Verify the return value's basic structure + + assert!(identity.secret().is_none()); + assert_eq!(StatusCode::NO_CONTENT, status); +} + +#[tokio::test] +async fn invalid_token() { + // Set up the environment + + let app = fixtures::scratch_app().await; + + // Call the endpoint + + let identity = fixtures::cookie::fictitious(); + let post::Error(error) = post::handler(State(app), fixtures::now(), identity, Json::default()) + .await + .expect_err("logged out with an invalid token fails"); + + // Verify the return value's basic structure + + assert!(matches!(error, app::ValidateError::InvalidToken)); +} diff --git a/src/user/routes/mod.rs b/src/user/routes/mod.rs new file mode 100644 index 0000000..ade96cb --- /dev/null +++ b/src/user/routes/mod.rs @@ -0,0 +1,14 @@ +use axum::{Router, routing::post}; + +use crate::app::App; + +mod login; +mod logout; +mod password; + +pub fn router() -> Router { + Router::new() + .route("/api/password", post(password::post::handler)) + .route("/api/auth/login", post(login::post::handler)) + .route("/api/auth/logout", post(logout::post::handler)) +} diff --git a/src/user/routes/password/mod.rs b/src/user/routes/password/mod.rs new file mode 100644 index 0000000..36b384e --- /dev/null +++ b/src/user/routes/password/mod.rs @@ -0,0 +1,4 @@ +pub mod post; + +#[cfg(test)] +mod test; diff --git a/src/user/routes/password/post.rs b/src/user/routes/password/post.rs new file mode 100644 index 0000000..296f6cd --- /dev/null +++ b/src/user/routes/password/post.rs @@ -0,0 +1,54 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use crate::{ + app::App, + clock::RequestedAt, + error::Internal, + token::{ + app, + extract::{Identity, IdentityCookie}, + }, + user::{Password, User}, +}; + +pub async fn handler( + State(app): State, + RequestedAt(now): RequestedAt, + identity: Identity, + cookie: IdentityCookie, + Json(request): Json, +) -> Result<(IdentityCookie, Json), Error> { + let (login, secret) = app + .tokens() + .change_password(&identity.user, &request.password, &request.to, &now) + .await + .map_err(Error)?; + let cookie = cookie.set(secret); + Ok((cookie, Json(login))) +} + +#[derive(serde::Deserialize)] +pub struct Request { + pub password: Password, + pub to: Password, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] pub app::LoginError); + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let Self(error) = self; + match error { + app::LoginError::Rejected => { + (StatusCode::BAD_REQUEST, "invalid name or password").into_response() + } + other => Internal::from(other).into_response(), + } + } +} diff --git a/src/user/routes/password/test.rs b/src/user/routes/password/test.rs new file mode 100644 index 0000000..01dcb38 --- /dev/null +++ b/src/user/routes/password/test.rs @@ -0,0 +1,68 @@ +use axum::extract::{Json, State}; + +use super::post; +use crate::{ + test::fixtures, + token::app::{LoginError, ValidateError}, +}; + +#[tokio::test] +async fn password_change() { + // Set up the environment + let app = fixtures::scratch_app().await; + let creds = fixtures::login::create_with_password(&app, &fixtures::now()).await; + let cookie = fixtures::cookie::logged_in(&app, &creds, &fixtures::now()).await; + let identity = fixtures::identity::from_cookie(&app, &cookie, &fixtures::now()).await; + + // Call the endpoint + let (name, password) = creds; + let to = fixtures::login::propose_password(); + let request = post::Request { + password: password.clone(), + to: to.clone(), + }; + let (new_cookie, Json(response)) = post::handler( + State(app.clone()), + fixtures::now(), + identity.clone(), + cookie.clone(), + Json(request), + ) + .await + .expect("changing passwords succeeds"); + + // Verify that we have a new session + assert_ne!(cookie.secret(), new_cookie.secret()); + + // Verify that we're still ourselves + assert_eq!(identity.user, response); + + // Verify that our original token is no longer valid + let validate_err = app + .tokens() + .validate( + &cookie + .secret() + .expect("original identity cookie has a secret"), + &fixtures::now(), + ) + .await + .expect_err("validating the original identity secret should fail"); + assert!(matches!(validate_err, ValidateError::InvalidToken)); + + // Verify that our original password is no longer valid + let login_err = app + .tokens() + .login(&name, &password, &fixtures::now()) + .await + .expect_err("logging in with the original password should fail"); + assert!(matches!(login_err, LoginError::Rejected)); + + // Verify that our new password is valid + let (login, _) = app + .tokens() + .login(&name, &to, &fixtures::now()) + .await + .expect("logging in with the new password should succeed"); + assert_eq!(identity.user, login); +} diff --git a/src/user/snapshot.rs b/src/user/snapshot.rs new file mode 100644 index 0000000..d548e06 --- /dev/null +++ b/src/user/snapshot.rs @@ -0,0 +1,52 @@ +use super::{ + Id, + event::{Created, Event}, +}; +use crate::name::Name; + +// This also implements FromRequestParts (see `./extract.rs`). As a result, it +// can be used as an extractor for endpoints that want to require a user, or for +// endpoints that need to behave differently depending on whether the client is +// or is not logged in. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +pub struct User { + pub id: Id, + pub name: Name, + // The omission of the hashed password is deliberate, to minimize the + // chance that it ends up tangled up in debug output or in some other chunk + // of logic elsewhere. +} + +impl User { + // Without this allow, clippy wants the `Option` return type to be `Self`. It's not a bad + // suggestion, but we need `Option` here, for two reasons: + // + // 1. This is used to collect streams using a fold, below, which requires a type + // consistent with the fold, and + // 2. It's also consistent with the other history state machine types. + #[allow(clippy::unnecessary_wraps)] + fn apply(state: Option, event: Event) -> Option { + match (state, event) { + (None, Event::Created(event)) => Some(event.into()), + (state, event) => panic!("invalid message event {event:#?} for state {state:#?}"), + } + } +} + +impl FromIterator for Option { + fn from_iter>(events: I) -> Self { + events.into_iter().fold(None, User::apply) + } +} + +impl From<&Created> for User { + fn from(event: &Created) -> Self { + event.user.clone() + } +} + +impl From for User { + fn from(event: Created) -> Self { + event.user + } +} diff --git a/src/user/validate.rs b/src/user/validate.rs new file mode 100644 index 0000000..0c97293 --- /dev/null +++ b/src/user/validate.rs @@ -0,0 +1,23 @@ +use unicode_segmentation::UnicodeSegmentation as _; + +use crate::name::Name; + +// Picked out of a hat. The power of two is not meaningful. +const NAME_TOO_LONG: usize = 64; + +pub fn name(name: &Name) -> bool { + let display = name.display(); + + [ + display.graphemes(true).count() < NAME_TOO_LONG, + display.chars().all(|ch| !ch.is_control()), + display.chars().next().is_some_and(|c| !c.is_whitespace()), + display.chars().last().is_some_and(|c| !c.is_whitespace()), + display + .chars() + .zip(display.chars().skip(1)) + .all(|(a, b)| !(a.is_whitespace() && b.is_whitespace())), + ] + .into_iter() + .all(|value| value) +} -- cgit v1.2.3 From f6191926ba94b8a1b303bca7c6996dce67781290 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Sun, 23 Mar 2025 16:18:24 -0400 Subject: Change the prefix for newly-generated user IDs to `U`, for `User`. --- src/user/id.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/user/id.rs b/src/user/id.rs index 9455deb..bc14c1f 100644 --- a/src/user/id.rs +++ b/src/user/id.rs @@ -1,6 +1,7 @@ use crate::id::Id as BaseId; -// Stable identifier for a User. Prefixed with `L`. +// Stable identifier for a User. Prefixed with `U`. Users created before March, 2025 may have an `L` +// prefix, instead. #[derive(Clone, Debug, Eq, PartialEq, sqlx::Type, serde::Serialize)] #[sqlx(transparent)] pub struct Id(BaseId); @@ -13,7 +14,7 @@ impl From for Id { impl Id { pub fn generate() -> Self { - BaseId::generate("L") + BaseId::generate("U") } } -- cgit v1.2.3 From 5e4e052c400bb88933125f3549cec6dc12a9d09b Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Sun, 23 Mar 2025 19:06:43 -0400 Subject: Rename `login` to `user` throughout the server --- src/boot/app.rs | 2 +- src/boot/mod.rs | 2 +- src/boot/routes/get.rs | 4 +-- src/boot/routes/test.rs | 12 ++++---- src/channel/routes/channel/test/delete.rs | 2 +- src/event/app.rs | 6 ++-- src/event/mod.rs | 6 ++-- src/event/routes/test/invite.rs | 16 +++++----- src/event/routes/test/message.rs | 18 +++++------ src/event/routes/test/resume.rs | 4 +-- src/event/routes/test/setup.rs | 6 ++-- src/event/routes/test/token.rs | 12 ++++---- src/invite/routes/invite/test/get.rs | 4 +-- src/invite/routes/invite/test/post.rs | 28 ++++++++--------- src/message/routes/message/test.rs | 8 ++--- src/setup/routes/test.rs | 10 +++--- src/test/fixtures/event.rs | 6 ++-- src/test/fixtures/identity.rs | 4 +-- src/test/fixtures/login.rs | 51 ------------------------------- src/test/fixtures/mod.rs | 2 +- src/test/fixtures/user.rs | 51 +++++++++++++++++++++++++++++++ src/user/routes/login/test.rs | 10 +++--- src/user/routes/logout/test.rs | 2 +- src/user/routes/password/test.rs | 4 +-- 24 files changed, 135 insertions(+), 135 deletions(-) delete mode 100644 src/test/fixtures/login.rs create mode 100644 src/test/fixtures/user.rs diff --git a/src/boot/app.rs b/src/boot/app.rs index 9c2559e..264d5ae 100644 --- a/src/boot/app.rs +++ b/src/boot/app.rs @@ -45,7 +45,7 @@ impl<'a> Boot<'a> { Ok(Snapshot { resume_point, - logins, + users: logins, channels, messages, }) diff --git a/src/boot/mod.rs b/src/boot/mod.rs index d614df5..c52b088 100644 --- a/src/boot/mod.rs +++ b/src/boot/mod.rs @@ -8,7 +8,7 @@ pub use self::routes::router; #[derive(serde::Serialize)] pub struct Snapshot { pub resume_point: Sequence, - pub logins: Vec, + pub users: Vec, pub channels: Vec, pub messages: Vec, } diff --git a/src/boot/routes/get.rs b/src/boot/routes/get.rs index c04c6b3..4873b7a 100644 --- a/src/boot/routes/get.rs +++ b/src/boot/routes/get.rs @@ -8,14 +8,14 @@ use crate::{app::App, boot::Snapshot, error::Internal, token::extract::Identity, pub async fn handler(State(app): State, identity: Identity) -> Result { let snapshot = app.boot().snapshot().await?; Ok(Response { - login: identity.user, + user: identity.user, snapshot, }) } #[derive(serde::Serialize)] pub struct Response { - pub login: User, + pub user: User, #[serde(flatten)] pub snapshot: Snapshot, } diff --git a/src/boot/routes/test.rs b/src/boot/routes/test.rs index 5bd9f66..55802fe 100644 --- a/src/boot/routes/test.rs +++ b/src/boot/routes/test.rs @@ -12,20 +12,20 @@ async fn returns_identity() { .await .expect("boot always succeeds"); - assert_eq!(viewer.user, response.login); + assert_eq!(viewer.user, response.user); } #[tokio::test] async fn includes_logins() { let app = fixtures::scratch_app().await; - let spectator = fixtures::login::create(&app, &fixtures::now()).await; + let spectator = fixtures::user::create(&app, &fixtures::now()).await; let viewer = fixtures::identity::fictitious(); let response = get::handler(State(app), viewer) .await .expect("boot always succeeds"); - assert!(response.snapshot.logins.contains(&spectator)); + assert!(response.snapshot.users.contains(&spectator)); } #[tokio::test] @@ -44,7 +44,7 @@ async fn includes_channels() { #[tokio::test] async fn includes_messages() { let app = fixtures::scratch_app().await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; let message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await; @@ -59,7 +59,7 @@ async fn includes_messages() { #[tokio::test] async fn excludes_expired_messages() { let app = fixtures::scratch_app().await; - let sender = fixtures::login::create(&app, &fixtures::ancient()).await; + let sender = fixtures::user::create(&app, &fixtures::ancient()).await; let channel = fixtures::channel::create(&app, &fixtures::ancient()).await; let expired_message = fixtures::message::send(&app, &channel, &sender, &fixtures::ancient()).await; @@ -80,7 +80,7 @@ async fn excludes_expired_messages() { #[tokio::test] async fn excludes_deleted_messages() { let app = fixtures::scratch_app().await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; let deleted_message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await; diff --git a/src/channel/routes/channel/test/delete.rs b/src/channel/routes/channel/test/delete.rs index 77a0b03..bd9261d 100644 --- a/src/channel/routes/channel/test/delete.rs +++ b/src/channel/routes/channel/test/delete.rs @@ -156,7 +156,7 @@ pub async fn channel_not_empty() { let app = fixtures::scratch_app().await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await; // Send the request diff --git a/src/event/app.rs b/src/event/app.rs index 447a98f..45a9099 100644 --- a/src/event/app.rs +++ b/src/event/app.rs @@ -33,8 +33,8 @@ impl<'a> Events<'a> { let mut tx = self.db.begin().await?; - let logins = tx.users().replay(resume_at).await?; - let login_events = logins + let users = tx.users().replay(resume_at).await?; + let user_events = users .iter() .map(user::History::events) .kmerge_by(Sequence::merge) @@ -57,7 +57,7 @@ impl<'a> Events<'a> { .filter(Sequence::after(resume_at)) .map(Event::from); - let replay_events = login_events + let replay_events = user_events .merge_by(channel_events, Sequence::merge) .merge_by(message_events, Sequence::merge) .collect::>(); diff --git a/src/event/mod.rs b/src/event/mod.rs index 773adc3..3ab88ec 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -16,7 +16,7 @@ pub use self::{ #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum Event { - Login(user::Event), + User(user::Event), Channel(channel::Event), Message(message::Event), } @@ -24,7 +24,7 @@ pub enum Event { impl Sequenced for Event { fn instant(&self) -> Instant { match self { - Self::Login(event) => event.instant(), + Self::User(event) => event.instant(), Self::Channel(event) => event.instant(), Self::Message(event) => event.instant(), } @@ -33,7 +33,7 @@ impl Sequenced for Event { impl From for Event { fn from(event: user::Event) -> Self { - Self::Login(event) + Self::User(event) } } diff --git a/src/event/routes/test/invite.rs b/src/event/routes/test/invite.rs index 80b4291..1d1bec6 100644 --- a/src/event/routes/test/invite.rs +++ b/src/event/routes/test/invite.rs @@ -12,7 +12,7 @@ async fn accepting_invite() { // Set up the environment let app = fixtures::scratch_app().await; - let issuer = fixtures::login::create(&app, &fixtures::now()).await; + let issuer = fixtures::user::create(&app, &fixtures::now()).await; let invite = fixtures::invite::issue(&app, &issuer, &fixtures::now()).await; let resume_point = fixtures::boot::resume_point(&app).await; @@ -30,7 +30,7 @@ async fn accepting_invite() { // Accept the invite - let (name, password) = fixtures::login::propose(); + let (name, password) = fixtures::user::propose(); let (joiner, _) = app .invites() .accept(&invite.id, &name, &password, &fixtures::now()) @@ -40,8 +40,8 @@ async fn accepting_invite() { // Expect a login created event let _ = events - .filter_map(fixtures::event::login) - .filter_map(fixtures::event::login::created) + .filter_map(fixtures::event::user) + .filter_map(fixtures::event::user::created) .filter(|event| future::ready(event.user == joiner)) .next() .expect_some("a login created event is sent") @@ -53,13 +53,13 @@ async fn previously_accepted_invite() { // Set up the environment let app = fixtures::scratch_app().await; - let issuer = fixtures::login::create(&app, &fixtures::now()).await; + let issuer = fixtures::user::create(&app, &fixtures::now()).await; let invite = fixtures::invite::issue(&app, &issuer, &fixtures::now()).await; let resume_point = fixtures::boot::resume_point(&app).await; // Accept the invite - let (name, password) = fixtures::login::propose(); + let (name, password) = fixtures::user::propose(); let (joiner, _) = app .invites() .accept(&invite.id, &name, &password, &fixtures::now()) @@ -81,8 +81,8 @@ async fn previously_accepted_invite() { // Expect a login created event let _ = events - .filter_map(fixtures::event::login) - .filter_map(fixtures::event::login::created) + .filter_map(fixtures::event::user) + .filter_map(fixtures::event::user::created) .filter(|event| future::ready(event.user == joiner)) .next() .expect_some("a login created event is sent") diff --git a/src/event/routes/test/message.rs b/src/event/routes/test/message.rs index fafaeb3..84a3aec 100644 --- a/src/event/routes/test/message.rs +++ b/src/event/routes/test/message.rs @@ -32,7 +32,7 @@ async fn sending() { // Send a message - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let message = app .messages() .send( @@ -65,7 +65,7 @@ async fn previously_sent() { // Send a message - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let message = app .messages() .send( @@ -105,7 +105,7 @@ async fn sent_in_multiple_channels() { // Set up the environment let app = fixtures::scratch_app().await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let resume_point = fixtures::boot::resume_point(&app).await; let channels = [ @@ -156,7 +156,7 @@ async fn sent_sequentially() { let app = fixtures::scratch_app().await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let resume_point = fixtures::boot::resume_point(&app).await; let messages = vec![ @@ -200,7 +200,7 @@ async fn expiring() { let app = fixtures::scratch_app().await; let channel = fixtures::channel::create(&app, &fixtures::ancient()).await; - let sender = fixtures::login::create(&app, &fixtures::ancient()).await; + let sender = fixtures::user::create(&app, &fixtures::ancient()).await; let message = fixtures::message::send(&app, &channel, &sender, &fixtures::ancient()).await; let resume_point = fixtures::boot::resume_point(&app).await; @@ -239,7 +239,7 @@ async fn previously_expired() { let app = fixtures::scratch_app().await; let channel = fixtures::channel::create(&app, &fixtures::ancient()).await; - let sender = fixtures::login::create(&app, &fixtures::ancient()).await; + let sender = fixtures::user::create(&app, &fixtures::ancient()).await; let message = fixtures::message::send(&app, &channel, &sender, &fixtures::ancient()).await; let resume_point = fixtures::boot::resume_point(&app).await; @@ -278,7 +278,7 @@ async fn deleting() { let app = fixtures::scratch_app().await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await; let resume_point = fixtures::boot::resume_point(&app).await; @@ -317,7 +317,7 @@ async fn previously_deleted() { let app = fixtures::scratch_app().await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await; let resume_point = fixtures::boot::resume_point(&app).await; @@ -356,7 +356,7 @@ async fn previously_purged() { let app = fixtures::scratch_app().await; let channel = fixtures::channel::create(&app, &fixtures::ancient()).await; - let sender = fixtures::login::create(&app, &fixtures::ancient()).await; + let sender = fixtures::user::create(&app, &fixtures::ancient()).await; let message = fixtures::message::send(&app, &channel, &sender, &fixtures::ancient()).await; let resume_point = fixtures::boot::resume_point(&app).await; diff --git a/src/event/routes/test/resume.rs b/src/event/routes/test/resume.rs index dc27691..633eae3 100644 --- a/src/event/routes/test/resume.rs +++ b/src/event/routes/test/resume.rs @@ -15,7 +15,7 @@ async fn resume() { let app = fixtures::scratch_app().await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let resume_point = fixtures::boot::resume_point(&app).await; let initial_message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await; @@ -96,7 +96,7 @@ async fn serial_resume() { // Set up the environment let app = fixtures::scratch_app().await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let channel_a = fixtures::channel::create(&app, &fixtures::now()).await; let channel_b = fixtures::channel::create(&app, &fixtures::now()).await; let resume_point = fixtures::boot::resume_point(&app).await; diff --git a/src/event/routes/test/setup.rs b/src/event/routes/test/setup.rs index 345018e..1170fe4 100644 --- a/src/event/routes/test/setup.rs +++ b/src/event/routes/test/setup.rs @@ -19,7 +19,7 @@ async fn previously_completed() { // Complete initial setup - let (name, password) = fixtures::login::propose(); + let (name, password) = fixtures::user::propose(); let (owner, _) = app .setup() .initial(&name, &password, &fixtures::now()) @@ -41,8 +41,8 @@ async fn previously_completed() { // Expect a login created event let _ = events - .filter_map(fixtures::event::login) - .filter_map(fixtures::event::login::created) + .filter_map(fixtures::event::user) + .filter_map(fixtures::event::user::created) .filter(|event| future::ready(event.user == owner)) .next() .expect_some("a login created event is sent") diff --git a/src/event/routes/test/token.rs b/src/event/routes/test/token.rs index d2232a4..a467de5 100644 --- a/src/event/routes/test/token.rs +++ b/src/event/routes/test/token.rs @@ -13,12 +13,12 @@ async fn terminates_on_token_expiry() { let app = fixtures::scratch_app().await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let resume_point = fixtures::boot::resume_point(&app).await; // Subscribe via the endpoint - let subscriber_creds = fixtures::login::create_with_password(&app, &fixtures::now()).await; + let subscriber_creds = fixtures::user::create_with_password(&app, &fixtures::now()).await; let subscriber = fixtures::identity::logged_in(&app, &subscriber_creds, &fixtures::ancient()).await; @@ -60,7 +60,7 @@ async fn terminates_on_logout() { let app = fixtures::scratch_app().await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let resume_point = fixtures::boot::resume_point(&app).await; // Subscribe via the endpoint @@ -106,12 +106,12 @@ async fn terminates_on_password_change() { let app = fixtures::scratch_app().await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let resume_point = fixtures::boot::resume_point(&app).await; // Subscribe via the endpoint - let creds = fixtures::login::create_with_password(&app, &fixtures::now()).await; + let creds = fixtures::user::create_with_password(&app, &fixtures::now()).await; let cookie = fixtures::cookie::logged_in(&app, &creds, &fixtures::now()).await; let subscriber = fixtures::identity::from_cookie(&app, &cookie, &fixtures::now()).await; @@ -127,7 +127,7 @@ async fn terminates_on_password_change() { // Verify the resulting stream's behaviour let (_, password) = creds; - let to = fixtures::login::propose_password(); + let to = fixtures::user::propose_password(); app.tokens() .change_password(&subscriber.user, &password, &to, &fixtures::now()) .await diff --git a/src/invite/routes/invite/test/get.rs b/src/invite/routes/invite/test/get.rs index c6780ed..0dc8a79 100644 --- a/src/invite/routes/invite/test/get.rs +++ b/src/invite/routes/invite/test/get.rs @@ -7,7 +7,7 @@ async fn valid_invite() { // Set up the environment let app = fixtures::scratch_app().await; - let issuer = fixtures::login::create(&app, &fixtures::now()).await; + let issuer = fixtures::user::create(&app, &fixtures::now()).await; let invite = fixtures::invite::issue(&app, &issuer, &fixtures::now()).await; // Call endpoint @@ -45,7 +45,7 @@ async fn expired_invite() { // Set up the environment let app = fixtures::scratch_app().await; - let issuer = fixtures::login::create(&app, &fixtures::ancient()).await; + let issuer = fixtures::user::create(&app, &fixtures::ancient()).await; let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await; app.invites() diff --git a/src/invite/routes/invite/test/post.rs b/src/invite/routes/invite/test/post.rs index 3db764c..b204b32 100644 --- a/src/invite/routes/invite/test/post.rs +++ b/src/invite/routes/invite/test/post.rs @@ -11,12 +11,12 @@ async fn valid_invite() { // Set up the environment let app = fixtures::scratch_app().await; - let issuer = fixtures::login::create(&app, &fixtures::now()).await; + let issuer = fixtures::user::create(&app, &fixtures::now()).await; let invite = fixtures::invite::issue(&app, &issuer, &fixtures::now()).await; // Call the endpoint - let (name, password) = fixtures::login::propose(); + let (name, password) = fixtures::user::propose(); let identity = fixtures::cookie::not_logged_in(); let request = post::Request { name: name.clone(), @@ -68,7 +68,7 @@ async fn nonexistent_invite() { // Call the endpoint - let (name, password) = fixtures::login::propose(); + let (name, password) = fixtures::user::propose(); let identity = fixtures::cookie::not_logged_in(); let request = post::Request { name: name.clone(), @@ -94,7 +94,7 @@ async fn expired_invite() { // Set up the environment let app = fixtures::scratch_app().await; - let issuer = fixtures::login::create(&app, &fixtures::ancient()).await; + let issuer = fixtures::user::create(&app, &fixtures::ancient()).await; let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await; app.invites() @@ -104,7 +104,7 @@ async fn expired_invite() { // Call the endpoint - let (name, password) = fixtures::login::propose(); + let (name, password) = fixtures::user::propose(); let identity = fixtures::cookie::not_logged_in(); let request = post::Request { name: name.clone(), @@ -130,10 +130,10 @@ async fn accepted_invite() { // Set up the environment let app = fixtures::scratch_app().await; - let issuer = fixtures::login::create(&app, &fixtures::ancient()).await; + let issuer = fixtures::user::create(&app, &fixtures::ancient()).await; let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await; - let (name, password) = fixtures::login::propose(); + let (name, password) = fixtures::user::propose(); app.invites() .accept(&invite.id, &name, &password, &fixtures::now()) .await @@ -141,7 +141,7 @@ async fn accepted_invite() { // Call the endpoint - let (name, password) = fixtures::login::propose(); + let (name, password) = fixtures::user::propose(); let identity = fixtures::cookie::not_logged_in(); let request = post::Request { name: name.clone(), @@ -167,14 +167,14 @@ async fn conflicting_name() { // Set up the environment let app = fixtures::scratch_app().await; - let issuer = fixtures::login::create(&app, &fixtures::ancient()).await; + let issuer = fixtures::user::create(&app, &fixtures::ancient()).await; let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await; let existing_name = Name::from("rijksmuseum"); app.users() .create( &existing_name, - &fixtures::login::propose_password(), + &fixtures::user::propose_password(), &fixtures::now(), ) .await @@ -183,7 +183,7 @@ async fn conflicting_name() { // Call the endpoint let conflicting_name = Name::from("r\u{0133}ksmuseum"); - let password = fixtures::login::propose_password(); + let password = fixtures::user::propose_password(); let identity = fixtures::cookie::not_logged_in(); let request = post::Request { @@ -212,13 +212,13 @@ async fn invalid_name() { // Set up the environment let app = fixtures::scratch_app().await; - let issuer = fixtures::login::create(&app, &fixtures::now()).await; + let issuer = fixtures::user::create(&app, &fixtures::now()).await; let invite = fixtures::invite::issue(&app, &issuer, &fixtures::now()).await; // Call the endpoint - let name = fixtures::login::propose_invalid_name(); - let password = fixtures::login::propose_password(); + let name = fixtures::user::propose_invalid_name(); + let password = fixtures::user::propose_password(); let identity = fixtures::cookie::not_logged_in(); let request = post::Request { name: name.clone(), diff --git a/src/message/routes/message/test.rs b/src/message/routes/message/test.rs index 1abd91c..1888be7 100644 --- a/src/message/routes/message/test.rs +++ b/src/message/routes/message/test.rs @@ -62,7 +62,7 @@ pub async fn delete_deleted() { // Set up the environment let app = fixtures::scratch_app().await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; let message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await; @@ -93,7 +93,7 @@ pub async fn delete_expired() { // Set up the environment let app = fixtures::scratch_app().await; - let sender = fixtures::login::create(&app, &fixtures::ancient()).await; + let sender = fixtures::user::create(&app, &fixtures::ancient()).await; let channel = fixtures::channel::create(&app, &fixtures::ancient()).await; let message = fixtures::message::send(&app, &channel, &sender, &fixtures::ancient()).await; @@ -124,7 +124,7 @@ pub async fn delete_purged() { // Set up the environment let app = fixtures::scratch_app().await; - let sender = fixtures::login::create(&app, &fixtures::ancient()).await; + let sender = fixtures::user::create(&app, &fixtures::ancient()).await; let channel = fixtures::channel::create(&app, &fixtures::ancient()).await; let message = fixtures::message::send(&app, &channel, &sender, &fixtures::ancient()).await; @@ -160,7 +160,7 @@ pub async fn delete_not_sender() { // Set up the environment let app = fixtures::scratch_app().await; - let sender = fixtures::login::create(&app, &fixtures::now()).await; + let sender = fixtures::user::create(&app, &fixtures::now()).await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; let message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await; diff --git a/src/setup/routes/test.rs b/src/setup/routes/test.rs index 5794b78..e9f5cd6 100644 --- a/src/setup/routes/test.rs +++ b/src/setup/routes/test.rs @@ -11,7 +11,7 @@ async fn fresh_instance() { // Call the endpoint let identity = fixtures::cookie::not_logged_in(); - let (name, password) = fixtures::login::propose(); + let (name, password) = fixtures::user::propose(); let request = post::Request { name: name.clone(), password: password.clone(), @@ -52,11 +52,11 @@ async fn login_exists() { // Set up the environment let app = fixtures::scratch_app().await; - fixtures::login::create(&app, &fixtures::now()).await; + fixtures::user::create(&app, &fixtures::now()).await; // Call the endpoint let identity = fixtures::cookie::not_logged_in(); - let (name, password) = fixtures::login::propose(); + let (name, password) = fixtures::user::propose(); let request = post::Request { name, password }; let post::Error(error) = post::handler(State(app.clone()), fixtures::now(), identity, Json(request)) @@ -76,8 +76,8 @@ async fn invalid_name() { // Call the endpoint - let name = fixtures::login::propose_invalid_name(); - let password = fixtures::login::propose_password(); + let name = fixtures::user::propose_invalid_name(); + let password = fixtures::user::propose_password(); let identity = fixtures::cookie::not_logged_in(); let request = post::Request { name: name.clone(), diff --git a/src/test/fixtures/event.rs b/src/test/fixtures/event.rs index c6e5337..a30bb4b 100644 --- a/src/test/fixtures/event.rs +++ b/src/test/fixtures/event.rs @@ -16,9 +16,9 @@ pub fn message(event: Event) -> Ready> { }) } -pub fn login(event: Event) -> Ready> { +pub fn user(event: Event) -> Ready> { future::ready(match event { - Event::Login(event) => Some(event), + Event::User(event) => Some(event), _ => None, }) } @@ -65,7 +65,7 @@ pub mod message { } } -pub mod login { +pub mod user { use std::future::{self, Ready}; pub use crate::user::Event; diff --git a/src/test/fixtures/identity.rs b/src/test/fixtures/identity.rs index 7611066..0b2f978 100644 --- a/src/test/fixtures/identity.rs +++ b/src/test/fixtures/identity.rs @@ -11,7 +11,7 @@ use crate::{ }; pub async fn create(app: &App, created_at: &RequestedAt) -> Identity { - let credentials = fixtures::login::create_with_password(app, created_at).await; + let credentials = fixtures::user::create_with_password(app, created_at).await; logged_in(app, &credentials, created_at).await } @@ -41,7 +41,7 @@ pub async fn logged_in( pub fn fictitious() -> Identity { let token = token::Id::generate(); - let login = fixtures::login::fictitious(); + let login = fixtures::user::fictitious(); Identity { token, user: login } } diff --git a/src/test/fixtures/login.rs b/src/test/fixtures/login.rs deleted file mode 100644 index e668c95..0000000 --- a/src/test/fixtures/login.rs +++ /dev/null @@ -1,51 +0,0 @@ -use faker_rand::{en_us::internet, lorem::Paragraphs}; -use uuid::Uuid; - -use crate::{ - app::App, - clock::RequestedAt, - name::Name, - user::{self, Password, User}, -}; - -pub async fn create_with_password(app: &App, created_at: &RequestedAt) -> (Name, Password) { - let (name, password) = propose(); - let login = app - .users() - .create(&name, &password, created_at) - .await - .expect("should always succeed if the login is actually new"); - - (login.name, password) -} - -pub async fn create(app: &App, created_at: &RequestedAt) -> User { - let (name, password) = propose(); - app.users() - .create(&name, &password, created_at) - .await - .expect("should always succeed if the login is actually new") -} - -pub fn fictitious() -> User { - User { - id: user::Id::generate(), - name: propose_name(), - } -} - -pub fn propose() -> (Name, Password) { - (propose_name(), propose_password()) -} - -pub fn propose_invalid_name() -> Name { - rand::random::().to_string().into() -} - -fn propose_name() -> Name { - rand::random::().to_string().into() -} - -pub fn propose_password() -> Password { - Uuid::new_v4().to_string().into() -} diff --git a/src/test/fixtures/mod.rs b/src/test/fixtures/mod.rs index 57eee30..418bdb5 100644 --- a/src/test/fixtures/mod.rs +++ b/src/test/fixtures/mod.rs @@ -9,8 +9,8 @@ pub mod event; pub mod future; pub mod identity; pub mod invite; -pub mod login; pub mod message; +pub mod user; pub async fn scratch_app() -> App { let pool = db::prepare("sqlite::memory:", "sqlite::memory:") diff --git a/src/test/fixtures/user.rs b/src/test/fixtures/user.rs new file mode 100644 index 0000000..e668c95 --- /dev/null +++ b/src/test/fixtures/user.rs @@ -0,0 +1,51 @@ +use faker_rand::{en_us::internet, lorem::Paragraphs}; +use uuid::Uuid; + +use crate::{ + app::App, + clock::RequestedAt, + name::Name, + user::{self, Password, User}, +}; + +pub async fn create_with_password(app: &App, created_at: &RequestedAt) -> (Name, Password) { + let (name, password) = propose(); + let login = app + .users() + .create(&name, &password, created_at) + .await + .expect("should always succeed if the login is actually new"); + + (login.name, password) +} + +pub async fn create(app: &App, created_at: &RequestedAt) -> User { + let (name, password) = propose(); + app.users() + .create(&name, &password, created_at) + .await + .expect("should always succeed if the login is actually new") +} + +pub fn fictitious() -> User { + User { + id: user::Id::generate(), + name: propose_name(), + } +} + +pub fn propose() -> (Name, Password) { + (propose_name(), propose_password()) +} + +pub fn propose_invalid_name() -> Name { + rand::random::().to_string().into() +} + +fn propose_name() -> Name { + rand::random::().to_string().into() +} + +pub fn propose_password() -> Password { + Uuid::new_v4().to_string().into() +} diff --git a/src/user/routes/login/test.rs b/src/user/routes/login/test.rs index 7399796..d2e7ee2 100644 --- a/src/user/routes/login/test.rs +++ b/src/user/routes/login/test.rs @@ -8,7 +8,7 @@ async fn correct_credentials() { // Set up the environment let app = fixtures::scratch_app().await; - let (name, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await; + let (name, password) = fixtures::user::create_with_password(&app, &fixtures::now()).await; // Call the endpoint @@ -52,7 +52,7 @@ async fn invalid_name() { let identity = fixtures::cookie::not_logged_in(); let logged_in_at = fixtures::now(); - let (name, password) = fixtures::login::propose(); + let (name, password) = fixtures::user::propose(); let request = post::Request { name: name.clone(), password, @@ -72,7 +72,7 @@ async fn incorrect_password() { // Set up the environment let app = fixtures::scratch_app().await; - let login = fixtures::login::create(&app, &fixtures::now()).await; + let login = fixtures::user::create(&app, &fixtures::now()).await; // Call the endpoint @@ -80,7 +80,7 @@ async fn incorrect_password() { let identity = fixtures::cookie::not_logged_in(); let request = post::Request { name: login.name, - password: fixtures::login::propose_password(), + password: fixtures::user::propose_password(), }; let post::Error(error) = post::handler(State(app.clone()), logged_in_at, identity, Json(request)) @@ -97,7 +97,7 @@ async fn token_expires() { // Set up the environment let app = fixtures::scratch_app().await; - let (name, password) = fixtures::login::create_with_password(&app, &fixtures::now()).await; + let (name, password) = fixtures::user::create_with_password(&app, &fixtures::now()).await; // Call the endpoint diff --git a/src/user/routes/logout/test.rs b/src/user/routes/logout/test.rs index 775fa9f..ce93760 100644 --- a/src/user/routes/logout/test.rs +++ b/src/user/routes/logout/test.rs @@ -12,7 +12,7 @@ async fn successful() { let app = fixtures::scratch_app().await; let now = fixtures::now(); - let creds = fixtures::login::create_with_password(&app, &fixtures::now()).await; + let creds = fixtures::user::create_with_password(&app, &fixtures::now()).await; let identity = fixtures::cookie::logged_in(&app, &creds, &now).await; let secret = fixtures::cookie::secret(&identity); diff --git a/src/user/routes/password/test.rs b/src/user/routes/password/test.rs index 01dcb38..f977327 100644 --- a/src/user/routes/password/test.rs +++ b/src/user/routes/password/test.rs @@ -10,13 +10,13 @@ use crate::{ async fn password_change() { // Set up the environment let app = fixtures::scratch_app().await; - let creds = fixtures::login::create_with_password(&app, &fixtures::now()).await; + let creds = fixtures::user::create_with_password(&app, &fixtures::now()).await; let cookie = fixtures::cookie::logged_in(&app, &creds, &fixtures::now()).await; let identity = fixtures::identity::from_cookie(&app, &cookie, &fixtures::now()).await; // Call the endpoint let (name, password) = creds; - let to = fixtures::login::propose_password(); + let to = fixtures::user::propose_password(); let request = post::Request { password: password.clone(), to: to.clone(), -- cgit v1.2.3 From d581218e8907b9ae2b5df5457b47d788cd8a67ff Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Sun, 23 Mar 2025 19:23:18 -0400 Subject: Update the API docs to describe `user`s, not `login`s. --- docs/api/authentication.md | 97 +++++++++++++++------------- docs/api/boot.md | 108 +++++++++++++++---------------- docs/api/channels-messages.md | 52 ++++++--------- docs/api/events.md | 145 +++++++++++++++++++++++------------------- docs/api/initial-setup.md | 38 ++++++----- docs/api/invitations.md | 102 +++++++++++++++-------------- 6 files changed, 281 insertions(+), 261 deletions(-) diff --git a/docs/api/authentication.md b/docs/api/authentication.md index 93a8e52..7b5ebd7 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -11,43 +11,45 @@ stateDiagram-v2 Authenticated --> Unauthenticated : Token expired ``` -Authentication associates each authenticated request with a login. +Authentication associates each authenticated request with a user. -To create logins, see [initial setup](./initial-setup.md) and [invitations](./invitations.md). +To create users, see [initial setup](./initial-setup.md) and [invitations](./invitations.md). ## Names -The service handles login names using two separate forms. +The service handles user names using two separate forms. -The first form is as given in the request used to create the login. This form of the login name is used throughout the API, and the service will preserve the name as entered (other than applying normalization), so that users' preferences around capitalization and accent marks are preserved. +The first form is as given in the request used to create the user. This form of the use name is used throughout the API, and the service will preserve the name as entered (other than applying normalization), so that users' preferences around capitalization and accent marks are preserved. -The second form is a "canonical" form, used internally by the service to control uniqueness and match names to logins. The canonical form is both case-folded and normalized. +The second form is a "canonical" form, used internally by the service to control uniqueness and match names to users. The canonical form is both case-folded and normalized. The canonical form is not available to API clients, but its use has practical consequences: -* Names that differ only by case or only by code point sequence are treated as the same name. If the name is in use, changing the capitalization or changing the sequence of combining marks will not allow the creation of a second "identical" login. -* The login API accepts any name that canonicalizes to the form stored in the database, making login names effectively case-insensitive. - +* Names that differ only by case or only by code point sequence are treated as the same name. If the name is in use, changing the capitalization or changing the sequence of combining marks will not allow the creation of a second "identical" user. +* The login API accepts any name that canonicalizes to the form stored in the database, making user names effectively case-insensitive and composition-insensitive. ## Identity tokens -A login is primarily authenticated using its username and password. However, passwords are a sensitive, long-lived credential, and are also operationally expensive to verify; for routine access, requests are authenticated using an identity token, instead. +A user is primarily authenticated using its username and password. However, passwords are a sensitive, long-lived credential, and are also operationally expensive to verify; for routine access, requests are authenticated using an identity token, instead. -Tokens are issued by logging into the service, using the `/api/auth/login` endpoint. The `/api/auth/logout` endpoint immediately invalidates the token used to make a request to it. Tokens are also invalidated after seven days of inactivity. +Tokens are issued by logging into the service, using the `/api/auth/login` endpoint. The +`/api/auth/logout` endpoint immediately invalidates the token used to make a request to it. Tokens are also invalidated after seven days of inactivity. -To authenticate a request, include `cookie: identity=TOKEN SECRET` header in the request. For browser-based clients, this may happen automatically. +To authenticate a request, include +`cookie: identity=TOKEN SECRET` header in the request. For browser-based clients, this may happen automatically. ## Authentication failures -Unless the endpoint's documentation says otherwise, all endpoints require authentication. Making a request to any endpoint that requires authentication, either without a token, or with a token that is not valid or that has expired, causes the service to return a `401 Unauthorized` response, instead of the responses documented for the endpoint the request was intended for. The API will not take action on requests that fail authentication in this way. +Unless the endpoint's documentation says otherwise, all endpoints require authentication. Making a request to any endpoint that requires authentication, either without a token, or with a token that is not valid or that has expired, causes the service to return a +`401 Unauthorized` response, instead of the responses documented for the endpoint the request was intended for. The API will not take action on requests that fail authentication in this way. ## `POST /api/auth/login` -Authenticates the user using their login name and password. The login must exist before calling this endpoint. +Authenticates the user using their name and password. The user must exist before calling this endpoint. **This endpoint does not require an `identity` cookie.** @@ -55,50 +57,53 @@ Authenticates the user using their login name and password. The login must exist ```json { - "name": "example username", - "password": "the plaintext password", + "name": "example username", + "password": "the plaintext password" } ``` The request must have the following fields: -| Field | Type | Description | -|:-----------|:-------|:--| -| `name` | string | The login's name. | +| Field | Type | Description | +|:-----------|:-------|:-------------------------------------| +| `name` | string | The login's name. | | `password` | string | The login's password, in plain text. | ### Success -This endpoint will respond with a status of `200 Okay` when successful. The body of the response will be a JSON object describing the authenticated login: +This endpoint will respond with a status of +`200 Okay` when successful. The body of the response will be a JSON object describing the authenticated user: ```json { - "id": "Labcd1234", - "name": "Andrea" + "id": "Uabcd1234", + "name": "Andrea" } ``` The response will include the following fields: -| Field | Type | Description | -|:------------|:-------|:--| -| `id` | string | The authenticated login's ID. | -| `name` | string | The authenticated login's name. | +| Field | Type | Description | +|:-------|:-------|:-------------------------------| +| `id` | string | The authenticated user's ID. | +| `name` | string | The authenticated user's name. | -The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the login identified in the request. This token's value must be kept confidential. +The response will include a `Set-Cookie` header for the +`identity` cookie, providing the client with a newly-minted identity token associated with the user identified in the request. This token's value must be kept confidential. The cookie will expire if it is not used regularly. ### Authentication failure -This endpoint will respond with a status of `401 Unauthorized` if the login name and password do not correspond to an existing login. +This endpoint will respond with a status of +`401 Unauthorized` if the login name and password do not correspond to an existing user. ## `POST /api/auth/logout` -Invalidates the identity token used to make the request, logging the user out. +Invalidates the identity token used to make the request, logging the caller out. ### Request @@ -112,51 +117,55 @@ The request must be an empty JSON object. This endpoint will respond with a status of `204 No Content` when successful. -The response will include a `Set-Cookie` header that clears the `identity` cookie. Regardless of whether the client clears the cookie, the service also invalidates the token. +The response will include a `Set-Cookie` header that clears the +`identity` cookie. Regardless of whether the client clears the cookie, the service also invalidates the token. ## `POST /api/password` -Changes the current login's password, and invalidate all outstanding identity tokens. +Changes the current user's password, and invalidates all outstanding identity tokens. ### Request ```json { - "password": "my-old-password", - "to": "my-new-password" + "password": "my-old-password", + "to": "my-new-password" } ``` The request must have the following fields: -| Field | Type | Description | -|:-----------|:-------|:--| +| Field | Type | Description | +|:-----------|:-------|:-----------------------------------------------| | `password` | string | The login's _current_ password, in plain text. | -| `to` | string | The login's _new_ password, in plain text. | +| `to` | string | The login's _new_ password, in plain text. | ### Success -This endpoint will respond with a status of `200 Okay` when successful. The body of the response will be a JSON object describing the authenticated login: +This endpoint will respond with a status of +`200 Okay` when successful. The body of the response will be a JSON object describing the authenticated user: ```json { - "id": "Labcd1234", - "name": "Andrea" + "id": "Uabcd1234", + "name": "Andrea" } ``` The response will include the following fields: -| Field | Type | Description | -|:------------|:-------|:--| -| `id` | string | The authenticated login's ID. | -| `name` | string | The authenticated login's name. | +| Field | Type | Description | +|:-------|:-------|:-------------------------------| +| `id` | string | The authenticated user's ID. | +| `name` | string | The authenticated user's name. | -The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the login identified in the request. This token's value must be kept confidential. All previously-created identity cookies will cease to be valid. +The response will include a `Set-Cookie` header for the +`identity` cookie, providing the client with a newly-minted identity token associated with the login identified in the request. This token's value must be kept confidential. All previously-created identity cookies will cease to be valid. The cookie will expire if it is not used regularly. ### Authentication failure -This endpoint will respond with a status of `400 Bad Request` if the `password` does not match the login's current password. +This endpoint will respond with a status of `400 Bad Request` if the +`password` does not match the login's current password. diff --git a/docs/api/boot.md b/docs/api/boot.md index 88f2d5b..0c2dc08 100644 --- a/docs/api/boot.md +++ b/docs/api/boot.md @@ -18,14 +18,12 @@ sequenceDiagram API <<->>- Andrea: Disconnect ``` - Client initialization serves three purposes: -* It confirms that the client's [identity token](./authentication.md) is valid, and tells the client what login that token is associated with. +* It confirms that the client's [identity token](./authentication.md) is valid, and tells the client what user that token is associated with. * It provides an initial snapshot of the state of the service. * It provides a resume point for the [event stream](./events.md), which allows clients to consume events starting from the moment the snapshot was created. - ## `GET /api/boot` Returns the information needed to initialize a client. @@ -34,77 +32,77 @@ This method is also the recommended way to validate the client's identity token, ### Success -This endpoint will respond with a status of `200 Okay` when successful. The body of the response will be a JSON object containing the initial state for the client: +This endpoint will respond with a status of +`200 Okay` when successful. The body of the response will be a JSON object containing the initial state for the client: ```json { - "login": { - "name": "example username", - "id": "L1234abcd", - }, - "resume_point": 1312, - "logins": [ - { - "id": "L1234abcd", - "name": "example username" - } - ], - "channels": [ - { - "name": "nonsense and such", - "id": "C1234abcd", - } - ], - "messages": [ - { - "at": "2024-09-27T23:19:10.208147Z", - "channel": "C1234abcd", - "sender": "L1234abcd", - "id": "M1312acab", - "body": "beep" - } - ] + "user": { + "name": "example username", + "id": "U1234abcd" + }, + "resume_point": 1312, + "users": [ + { + "id": "U1234abcd", + "name": "example username" + } + ], + "channels": [ + { + "name": "nonsense and such", + "id": "C1234abcd" + } + ], + "messages": [ + { + "at": "2024-09-27T23:19:10.208147Z", + "channel": "C1234abcd", + "sender": "U1234abcd", + "id": "M1312acab", + "body": "beep" + } + ] } ``` The response will include the following fields: -| Field | Type | Description | -|:---------------|:----------------|:--| -| `login` | object | The details of the caller's identity. | +| Field | Type | Description | +|:---------------|:----------------|:-------------------------------------------------------------------------------------------------------------------------| +| `user` | object | The details of the caller's identity. | | `resume_point` | integer | A resume point for [events](./events.md), such that the event stream will begin immediately after the included snapshot. | -| `logins` | array of object | A snapshot of the logins present in the service. | -| `channels` | array of object | A snapshot of the channels present in the service. | -| `messages` | array of object | A snapshot of the messages present in the service. | +| `users` | array of object | A snapshot of the users present in the service. | +| `channels` | array of object | A snapshot of the channels present in the service. | +| `messages` | array of object | A snapshot of the messages present in the service. | -The `login` object will include the following fields: +The `user` object will include the following fields: -| Field | Type | Description | -|:-------|:-------|:--| +| Field | Type | Description | +|:-------|:-------|:-----------------------------------------| | `name` | string | The name of the caller's login identity. | -| `id` | string | The ID of the caller's login identity. | - +| `id` | string | The ID of the caller's login identity. | -Each element of the `logins` array describes a distinct login, and will include the following fields: +Each element of the `users` array describes a distinct user, and will include the following fields: -| Field | Type | Description | -|:-------|:-------|:--| -| `name` | string | The name for the login. | -| `id` | string | A unique identifier for the login. This can be used to associate the login with other events, or to make API calls targeting the login. | +| Field | Type | Description | +|:-------|:-------|:-------------------------------------------------------------------------------------------------------------------------------------| +| `name` | string | The name for the user. | +| `id` | string | A unique identifier for the user. This can be used to associate the user with other events, or to make API calls targeting the user. | Each element of the `channels` array describes a distinct channel, and will include the following fields: -| Field | Type | Description | -|:-------|:-------|:--| -| `name` | string | The name for the channel. | +| Field | Type | Description | +|:-------|:-------|:----------------------------------------------------------------------------------------------------------------------------------------------| +| `name` | string | The name for the channel. | | `id` | string | A unique identifier for the channel. This can be used to associate the channel with other events, or to make API calls targeting the channel. | Each element of the `messages` array describes a distinct message, and will include the following fields: -| Field | Type | Description | -|:----------|:----------|:--| -| `at` | timestamp | The moment the message was sent. | -| `channel` | string | The ID of the channel the message was sent to. | -| `sender` | string | The ID of the login that sent the message. | +| Field | Type | Description | +|:----------|:----------|:----------------------------------------------------------------------------------------------------------------------------------------------| +| `at` | timestamp | The moment the message was sent. | +| `channel` | string | The ID of the channel the message was sent to. | +| `sender` | string | The ID of the user that sent the message. | | `id` | string | A unique identifier for the message. This can be used to associate the message with other events, or to make API calls targeting the message. | -| `body` | string | The text of the message. | +| `body` | string | The text of the message. | diff --git a/docs/api/channels-messages.md b/docs/api/channels-messages.md index a3c90be..c5b90ab 100644 --- a/docs/api/channels-messages.md +++ b/docs/api/channels-messages.md @@ -31,25 +31,17 @@ Every channel has a unique name, chosen when the channel is created. The service handles channel names using two separate forms. -The first form is as given in the request used to create the channel. This form of the channel name is used throughout -the API, and the service will preserve the name as entered (other than applying normalization), so that users' -preferences around capitalization and accent marks are preserved. +The first form is as given in the request used to create the channel. This form of the channel name is used throughout the API, and the service will preserve the name as entered (other than applying normalization), so that users' preferences around capitalization and accent marks are preserved. -The second form is a "canonical" form, used internally by the service to control uniqueness and match names to channels. -The canonical form is both case-folded and normalized. +The second form is a "canonical" form, used internally by the service to control uniqueness and match names to channels. The canonical form is both case-folded and normalized. -The canonical form is not available to API clients, but its use has practical consequences. Names that differ only by -case or only by code point sequence are treated as the same name. If the name is in use, changing the capitalization or -changing the sequence of combining marks will not allow the creation of a second "identical" channel. +The canonical form is not available to API clients, but its use has practical consequences. Names that differ only by case or only by code point sequence are treated as the same name. If the name is in use, changing the capitalization or changing the sequence of combining marks will not allow the creation of a second "identical" channel. ## Expiry and purging -Both channels and messages expire after a time. Messages expire 90 days after being sent. Channels expire 90 days after -the last message sent to them, or after creation if no messages are sent in that time. +Both channels and messages expire after a time. Messages expire 90 days after being sent. Channels expire 90 days after the last message sent to them, or after creation if no messages are sent in that time. -Deleted channels and messages, including those that have expired, are temporarily retained by the service, to allow -clients that are not connected to receive the corresponding deletion [events](./events.md). To limit storage growth, -deleted channels and messages are purged from the service seven days after they were deleted. +Deleted channels and messages, including those that have expired, are temporarily retained by the service, to allow clients that are not connected to receive the corresponding deletion [events](./events.md). To limit storage growth, deleted channels and messages are purged from the service seven days after they were deleted. ## `POST /api/channels` @@ -79,8 +71,8 @@ The proposed `name` must be valid. The precise definition of valid is still up i ### Success -This endpoint will respond with a status of `202 Accepted` when successful. The body of the response will be a JSON -object describing the new channel: +This endpoint will respond with a status of +`202 Accepted` when successful. The body of the response will be a JSON object describing the new channel: ```json { @@ -96,9 +88,7 @@ The response will have the following fields: | `id` | string | A unique identifier for the channel. This can be used to associate the channel with events, or to make API calls targeting the channel. | | `name` | string | The channel's name. | -The returned name may not be identical to the name requested, as the name will be converted -to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this -normalization; the service will use the normalized name elsewhere, and does not store the originally requested name. +The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name. When completed, the service will emit a [channel created](events.md#channel-created) event with the channel's ID. @@ -136,14 +126,14 @@ The request must have the following fields: ### Success -This endpoint will respond with a status of `202 Accepted` when successful. The body of the response will be a JSON -object describing the newly-sent message: +This endpoint will respond with a status of +`202 Accepted` when successful. The body of the response will be a JSON object describing the newly-sent message: ```json { "at": "2024-10-19T04:37:09.467325Z", "channel": "Cfqdn1234", - "sender": "Labcd1234", + "sender": "Uabcd1234", "id": "Mgh98yp75", "body": "an elaborate example message" } @@ -155,13 +145,11 @@ The response will have the following fields: |:----------|:----------|:----------------------------------------------------------------------------------------------------------------------------------------| | `at` | timestamp | The moment the message was sent. | | `channel` | string | The ID of the channel the message was sent to. | -| `sender` | string | The ID of the login that sent the message. | +| `sender` | string | The ID of the user that sent the message. | | `id` | string | A unique identifier for the message. This can be used to associate the message with events, or to make API calls targeting the message. | | `body` | string | The message's body. | -The returned message body may not be identical to the body as sent, as the body will be converted -to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned body will include this -normalization; the service will use the normalized body elsewhere, and does not store the originally submitted body. +The returned message body may not be identical to the body as sent, as the body will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned body will include this normalization; the service will use the normalized body elsewhere, and does not store the originally submitted body. When completed, the service will emit a [message sent](events.md#message-sent) event with the message's ID. @@ -173,8 +161,7 @@ This endpoint will respond with a status of `404 Not Found` if the channel ID is Deletes a channel. -Deleting a channel prevents it from receiving any further messages. The channel must be empty; to delete a channel with -messages in it, delete the messages first (or wait for them to expire). +Deleting a channel prevents it from receiving any further messages. The channel must be empty; to delete a channel with messages in it, delete the messages first (or wait for them to expire). This endpoint requires the following path parameter: @@ -184,8 +171,8 @@ This endpoint requires the following path parameter: ### Success -This endpoint will respond with a status of `202 Accepted` when successful. The body of the response will be a JSON -object describing the deleted channel: +This endpoint will respond with a status of +`202 Accepted` when successful. The body of the response will be a JSON object describing the deleted channel: ```json { @@ -199,8 +186,7 @@ The response will have the following fields: |:------|:-------|:------------------| | `id` | string | The channel's ID. | -When completed, the service will emit a [message deleted](events.md#message-deleted) event for each message in the -channel, followed by a [channel deleted](events.md#channel-deleted) event with the channel's ID. +When completed, the service will emit a [message deleted](events.md#message-deleted) event for each message in the channel, followed by a [channel deleted](events.md#channel-deleted) event with the channel's ID. ### Channel not empty @@ -222,8 +208,8 @@ This endpoint requires the following path parameter: ### Success -This endpoint will respond with a status of `202 Accepted` when successful. The body of the response will be a JSON -object describing the deleted message: +This endpoint will respond with a status of +`202 Accepted` when successful. The body of the response will be a JSON object describing the deleted message: ```json { diff --git a/docs/api/events.md b/docs/api/events.md index b23469c..3347a26 100644 --- a/docs/api/events.md +++ b/docs/api/events.md @@ -28,12 +28,14 @@ sequenceDiagram end ``` -The core of the service is to facilitate conversations between logins. Conversational activity is delivered to clients using _events_. Each event notifies interested clients of activity sent to the service through its API. +The core of the service is to facilitate conversations between users. Conversational activity is delivered to clients using +_events_. Each event notifies interested clients of activity sent to the service through its API. ## Asynchronous completion -A number of endpoints return `202 Accepted` responses. The actions performed by those endpoints will be completed before events are delivered. To await the completion of an operation which returns this response, clients must monitor the event stream for the corresponding event. +A number of endpoints return +`202 Accepted` responses. The actions performed by those endpoints will be completed before events are delivered. To await the completion of an operation which returns this response, clients must monitor the event stream for the corresponding event. ## `GET /api/events` @@ -46,11 +48,14 @@ This endpoint is designed for use with the [EventSource] DOM API, and supports s ### Query parameters -This endpoint requires a `resume_point` (integer) query parameter. The event stream will collect events published after that point in time. The value must be obtained by calling the [`GET /api/boot`](./boot.md) method. +This endpoint requires a +`resume_point` (integer) query parameter. The event stream will collect events published after that point in time. The value must be obtained by calling the [ +`GET /api/boot`](./boot.md) method. ### Request headers -This endpoint accepts an optional `last-event-id` (string) header. If present, the value must be the value of the `id` field of the last message processed by the client. The returned event stream will begin with the following message. If absent, the returned event stream will begin from the start of the event collection. +This endpoint accepts an optional `last-event-id` (string) header. If present, the value must be the value of the +`id` field of the last message processed by the client. The returned event stream will begin with the following message. If absent, the returned event stream will begin from the start of the event collection. This header is set automatically by `EventSource` when reconnecting to an event stream. @@ -67,7 +72,7 @@ data: "type": "message", data: "event": "sent", data: "at": "2024-09-27T23:19:10.208147Z", data: "channel": "C9876cyyz", -data: "sender": "L1234abcd", +data: "sender": "U1234abcd", data: "id": "M1312acab", data: "body": "beep" data: } @@ -75,46 +80,47 @@ data: } The service will keep the connection open, and will deliver events as they occur. -The service may terminate the connection at any time. Clients should reconnect and resume the stream, using the `last-event-id` header to resume from the last message received. The `id` of each event is an ephemeral ID, useful only for this purpose. +The service may terminate the connection at any time. Clients should reconnect and resume the stream, using the +`last-event-id` header to resume from the last message received. The +`id` of each event is an ephemeral ID, useful only for this purpose. Each event's `data` consists of a JSON object describing one event. Every event includes the following fields: -| Field | Type | Description | -|:--------|:-------|:--| -| `type` | string | The type of entity the event describes. Will be one of the types listed in the next section. | +| Field | Type | Description | +|:--------|:-------|:-------------------------------------------------------------------------------------------------------------| +| `type` | string | The type of entity the event describes. Will be one of the types listed in the next section. | | `event` | string | The specific kind of event. Will be one of the events listed with the associated `type` in the next section. | The remaining fields depend on the `type` and `event` field. -## Login events +## User events -The following events describe changes to logins. +The following events describe changes to users. -These events have the `type` field set to `"login"`. +These events have the `type` field set to `"user"`. -### Login created +### User created ```json { - "type": "login", - "event": "created", - "at": "2024-09-27T23:17:10.208147Z", - "id": "L1234abcd", - "name": "example username" + "type": "user", + "event": "created", + "at": "2024-09-27T23:17:10.208147Z", + "id": "U1234abcd", + "name": "example username" } ``` -Sent whenever a new login is created. +Sent whenever a new user is created. These events have the `event` field set to `"created"`. They include the following additional fields: -| Field | Type | Description | -|:-------|:----------|:--| -| `at` | timestamp | The moment the login was created. | -| `id` | string | A unique identifier for the newly-created login. This can be used to associate the login with other events, or to make API calls targeting the login. | -| `name` | string | The login's name. | - +| Field | Type | Description | +|:-------|:----------|:---------------------------------------------------------------------------------------------------------------------------------------------------| +| `at` | timestamp | The moment the user was created. | +| `id` | string | A unique identifier for the newly-created user. This can be used to associate the user with other events, or to make API calls targeting the user. | +| `name` | string | The user's name. | ## Channel events @@ -126,11 +132,11 @@ These events have the `type` field set to `"channel"`. ```json { - "type": "channel", - "event": "created", - "at": "2024-09-27T23:18:10.208147Z", - "id": "C9876cyyz", - "name": "example channel 2" + "type": "channel", + "event": "created", + "at": "2024-09-27T23:18:10.208147Z", + "id": "C9876cyyz", + "name": "example channel 2" } ``` @@ -138,14 +144,18 @@ Sent whenever a new channel is created. These events have the `event` field set to `"created"`. They include the following additional fields: -| Field | Type | Description | -|:-------------|:--------------------|:--| -| `at` | timestamp | The moment the channel was created. | +| Field | Type | Description | +|:-------------|:--------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `at` | timestamp | The moment the channel was created. | | `id` | string | A unique identifier for the newly-created channel. This can be used to associate the channel with other events, or to make API calls targeting the channel. | -| `name` | string | The channel's name. | -| `deleted_at` | timestamp, optional | If set, the moment the channel was deleted. | +| `name` | string | The channel's name. | +| `deleted_at` | timestamp, optional | If set, the moment the channel was deleted. | -When a channel is deleted or expires, the `"created"` event is replaced with a tombstone `"created"` event, so that the original channel cannot be trivially recovered from the event stream. Tombstone events have a `deleted_at` field, and a `name` of `""`. Tombstone events for channels use an empty string as the name, and not `null` or with the `name` field removed entirely, to simplify client development. While clients _should_ treat deleted channels specially, for example by rendering them as "channel deleted" markers, they don't have to be - they make sense if interpreted as channels with empty names, too. +When a channel is deleted or expires, the `"created"` event is replaced with a tombstone +`"created"` event, so that the original channel cannot be trivially recovered from the event stream. Tombstone events have a +`deleted_at` field, and a `name` of `""`. Tombstone events for channels use an empty string as the name, and not +`null` or with the `name` field removed entirely, to simplify client development. While clients +_should_ treat deleted channels specially, for example by rendering them as "channel deleted" markers, they don't have to be - they make sense if interpreted as channels with empty names, too. Once a deleted channel is [purged](./channels-messages.md#expiry-and-purging), these tombstone events are removed from the event stream. @@ -153,10 +163,10 @@ Once a deleted channel is [purged](./channels-messages.md#expiry-and-purging), t ```json { - "type": "channel", - "event": "deleted", - "at": "2024-09-28T03:40:25.384318Z", - "id": "C9876cyyz" + "type": "channel", + "event": "deleted", + "at": "2024-09-28T03:40:25.384318Z", + "id": "C9876cyyz" } ``` @@ -164,11 +174,10 @@ Sent whenever a channel is deleted or expires. These events have the `event` field set to `"deleted"`. They include the following additional fields: -| Field | Type | Description | -|:------|:----------|:--| +| Field | Type | Description | +|:------|:----------|:------------------------------------| | `at` | timestamp | The moment the channel was deleted. | -| `id` | string | The deleted channel's ID. | - +| `id` | string | The deleted channel's ID. | ## Message events @@ -180,13 +189,13 @@ These events have the `type` field set to `"message"`. ```json { - "type": "message", - "event": "sent", - "at": "2024-09-27T23:19:10.208147Z", - "channel": "C9876cyyz", - "sender": "L1234abcd", - "id": "M1312acab", - "body": "an effusive blob of prose, condensed down to a single string" + "type": "message", + "event": "sent", + "at": "2024-09-27T23:19:10.208147Z", + "channel": "C9876cyyz", + "sender": "U1234abcd", + "id": "M1312acab", + "body": "an effusive blob of prose, condensed down to a single string" } ``` @@ -194,16 +203,20 @@ Sent whenever a message is sent by a client. These events have the `event` field set to `"sent"`. They include the following additional fields: -| Field | Type | Description | -|:-------------|:--------------------|:--| -| `at` | timestamp | The moment the message was sent. | -| `channel` | string | The ID of the channel the message was sent to. | -| `sender` | string | The ID of the login that sent the message. | +| Field | Type | Description | +|:-------------|:--------------------|:----------------------------------------------------------------------------------------------------------------------------------------------| +| `at` | timestamp | The moment the message was sent. | +| `channel` | string | The ID of the channel the message was sent to. | +| `sender` | string | The ID of the user that sent the message. | | `id` | string | A unique identifier for the message. This can be used to associate the message with other events, or to make API calls targeting the message. | -| `body` | string | The text of the message. | -| `deleted_at` | timestamp, optional | If set, the moment the message was deleted. | +| `body` | string | The text of the message. | +| `deleted_at` | timestamp, optional | If set, the moment the message was deleted. | -When a message is deleted or expires, the `"sent"` event is replaced with a tombstone `"sent"` event, so that the original message cannot be trivially recovered from the event stream. Tombstone events have a `deleted_at` field, and a `body` of `""`. Tombstone events for messages use an empty string as the `body`, and not `null` or with the `body` field removed entirely, to simplify client development. While clients _should_ treat deleted messages specially, for example by rendering them as "message deleted" markers, they don't have to be - they make sense if interpreted as messages with empty bodies, too. +When a message is deleted or expires, the `"sent"` event is replaced with a tombstone +`"sent"` event, so that the original message cannot be trivially recovered from the event stream. Tombstone events have a +`deleted_at` field, and a `body` of `""`. Tombstone events for messages use an empty string as the `body`, and not +`null` or with the `body` field removed entirely, to simplify client development. While clients +_should_ treat deleted messages specially, for example by rendering them as "message deleted" markers, they don't have to be - they make sense if interpreted as messages with empty bodies, too. Once a deleted message is [purged](./channels-messages.md#expiry-and-purging), these tombstone events are removed from the event stream. @@ -211,10 +224,10 @@ Once a deleted message is [purged](./channels-messages.md#expiry-and-purging), t ```json { - "type": "message", - "event": "deleted", - "at": "2024-09-28T02:44:27.077355Z", - "id": "M1312acab" + "type": "message", + "event": "deleted", + "at": "2024-09-28T02:44:27.077355Z", + "id": "M1312acab" } ``` @@ -222,7 +235,7 @@ Sent whenever a message is deleted or expires. These events have the `event` field set to `"deleted"`. They include the following additional fields: -| Field | Type | Description | -|:------|:----------|:--| +| Field | Type | Description | +|:------|:----------|:------------------------------------| | `at` | timestamp | The moment the message was deleted. | -| `id` | string | The deleted message's ID. | +| `id` | string | The deleted message's ID. | diff --git a/docs/api/initial-setup.md b/docs/api/initial-setup.md index c2bdaec..0179397 100644 --- a/docs/api/initial-setup.md +++ b/docs/api/initial-setup.md @@ -18,7 +18,8 @@ New instances of this service require an initial setup step before they can full ## Requests before setup completed -Before the service is set up, all API endpoints, other than those specifically documented as exceptions, will return a status of `503 Service Unavailable` to all requests. +Before the service is set up, all API endpoints, other than those specifically documented as exceptions, will return a status of +`503 Service Unavailable` to all requests. Initial setup can be completed only once. @@ -27,9 +28,9 @@ Initial setup can be completed only once. Initial setup performs the following tasks: -* Create the first login for the service. +* Create the first user for the service. - This is the only login that does not require an [invitation](./invitations.md). + This is the only user that does not require an [invitation](./invitations.md). **This endpoint does not require an `identity` cookie.** @@ -39,16 +40,16 @@ Initial setup performs the following tasks: ```json { - "name": "example username", - "password": "the plaintext password", + "name": "example username", + "password": "the plaintext password" } ``` The request must have the following fields: -| Field | Type | Description | -|:-----------|:-------|:--| -| `name` | string | The initial login's name. | +| Field | Type | Description | +|:-----------|:-------|:---------------------------------------------| +| `name` | string | The initial login's name. | | `password` | string | The initial login's password, in plain text. | @@ -65,27 +66,29 @@ The proposed `name` must be valid. The precise definition of valid is still up i -This endpoint will respond with a status of `200 Okay` when successful. The body of the response will be a JSON object describing the newly-created login: +This endpoint will respond with a status of +`200 Okay` when successful. The body of the response will be a JSON object describing the newly-created user: ```json { - "id": "Labcd1234", - "name": "Andrea" + "id": "Uabcd1234", + "name": "Andrea" } ``` The response will include the following fields: -| Field | Type | Description | -|:------------|:-------|:--| -| `id` | string | A unique identifier for the newly-created login. This can be used to associate the login with other events, or to make API calls targeting the login. | -| `name` | string | The login's name. | +| Field | Type | Description | +|:-------|:-------|:---------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | string | A unique identifier for the newly-created user. This can be used to associate the user with other events, or to make API calls targeting the user. | +| `name` | string | The user's name. | The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name. The provided password will also be converted to normalization form C. However, the normalized password is not returned to the client. -The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the initial login created for this request. See the [authentication](./authentication) section for details on how this cookie may be used. +The response will include a `Set-Cookie` header for the +`identity` cookie, providing the client with a newly-minted identity token associated with the initial user created for this request. See the [authentication](./authentication) section for details on how this cookie may be used. The cookie will expire if it is not used regularly. @@ -95,5 +98,6 @@ This endpoint will respond with a status of `400 Bad Request` if the proposed `n ### Setup previously completed -Once completed, this operation cannot be performed a second time. Subsequent requests to this endpoint will respond with a status of `409 Conflict`. +Once completed, this operation cannot be performed a second time. Subsequent requests to this endpoint will respond with a status of +`409 Conflict`. diff --git a/docs/api/invitations.md b/docs/api/invitations.md index 1839ef5..0be2c2e 100644 --- a/docs/api/invitations.md +++ b/docs/api/invitations.md @@ -21,9 +21,9 @@ sequenceDiagram API -->>- Blake : Success ``` -Other than the login created during [initial setup](./initial-setup.md), new logins can only be created by using invitations. +Other than the user created during [initial setup](./initial-setup.md), new users can only be created by using invitations. -Any login can create invitations. Each invitation can be accepted at most once. An invitation which is not accepted within 24 hours expires. +Any user can create invitations. Each invitation can be accepted at most once. An invitation which is not accepted within 24 hours expires. ## `POST /api/invite` @@ -40,25 +40,27 @@ The request must be an empty JSON object. ### Success -This endpoint will respond with a status of `200 Okay` when successful. The body of the response will be a JSON object describing the new invitation: +This endpoint will respond with a status of +`200 Okay` when successful. The body of the response will be a JSON object describing the new invitation: ```json { - "id": "I3884", - "issuer": "Labcd1234", - "issued_at": "2024-10-12T01:43:12.001853Z" + "id": "I3884", + "issuer": "Uabcd1234", + "issued_at": "2024-10-12T01:43:12.001853Z" } ``` The response will include the following fields: -| Field | Type | Description | -|:------------|:-------|:--| +| Field | Type | Description | +|:------------|:-------|:------------------------------------------------------------------------------| | `id` | string | A unique identifier for the invitation. This ID must be given to the invitee. | -| `issuer` | string | The login ID of the invitation's issuer. | -| `issued_at` | string | The timestamp from which the invitation will expire. | +| `issuer` | string | The user ID of the invitation's issuer. | +| `issued_at` | string | The timestamp from which the invitation will expire. | -Clients and their operators are responsible for delivering the invitation to the invitee. Clients are strongly recommended to construct a URL for the invitation so that the invitee can take action on it easily. The included client supports URLs of the format `https://example.net/invite/:id` (with the `:id` placeholder substituted with the invitation's ID). +Clients and their operators are responsible for delivering the invitation to the invitee. Clients are strongly recommended to construct a URL for the invitation so that the invitee can take action on it easily. The included client supports URLs of the format +`https://example.net/invite/:id` (with the `:id` placeholder substituted with the invitation's ID). ## `GET /api/invite/:id` @@ -67,50 +69,53 @@ Returns information about an outstanding invitation. This endpoint requires the following path parameter: -| Parameter | Type | Description | -|:----------|:-------|:--| -| `id` | string | An invitation ID, as returned from a previous request to `POST /api/invite`. | +| Parameter | Type | Description | +|:----------|:-------|:-----------------------------------------------------------------------------| +| `id` | string | An invitation ID, as returned from a previous request to `POST /api/invite`. | **This endpoint does not require an `identity` cookie.** ### On success -This endpoint will respond with a status of `200 Okay` when successful. The body of the response will be a JSON object describing the invitation: +This endpoint will respond with a status of +`200 Okay` when successful. The body of the response will be a JSON object describing the invitation: ```json { - "issuer": { - "id": "Labcd1234", - "name": "i send you invites" - }, - "issued_at": "2024-10-12T01:43:12.001853Z" + "issuer": { + "id": "Uabcd1234", + "name": "i send you invites" + }, + "issued_at": "2024-10-12T01:43:12.001853Z" } ``` The response will include the following fields: -| Field | Type | Description | -|:------------|:-------|:--| -| `id` | string | The ID of the invitation. | -| `issuer` | string | The login name of the invitation's issuer. | +| Field | Type | Description | +|:------------|:-------|:-----------------------------------------------------| +| `id` | string | The ID of the invitation. | +| `issuer` | string | The name of the invitation's issuer. | | `issued_at` | string | The timestamp from which the invitation will expire. | -Clients should present the `issuer` to the user when presenting an invitation, so as to personalize the invitation and help them understand their connection with the service. +Clients should present the +`issuer` to the user when presenting an invitation, so as to personalize the invitation and help them understand their connection with the service. ### Invitation not found -This endpoint will respond with a status of `404 Not Found` when the invitation ID either does not exist, or has already been accepted. +This endpoint will respond with a status of +`404 Not Found` when the invitation ID either does not exist, or has already been accepted. ## `POST /api/invite/:id` -Accepts an invitation and creates a new login. +Accepts an invitation and creates a new user. This endpoint requires the following path parameter: -| Parameter | Type | Description | -|:----------|:-------|:--| -| `id` | string | An invitation ID, as returned from a previous request to `POST /api/invite`. | +| Parameter | Type | Description | +|:----------|:-------|:-----------------------------------------------------------------------------| +| `id` | string | An invitation ID, as returned from a previous request to `POST /api/invite`. | **This endpoint does not require an `identity` cookie.** @@ -118,17 +123,17 @@ This endpoint requires the following path parameter: ```json { - "name": "example login", - "password": "correct-horse-battery-staple" + "name": "example user", + "password": "correct-horse-battery-staple" } ``` The request must have the following fields: -| Field | Type | Description | -|:-----------|:-------|:--| -| `name` | string | The new login's name. | -| `password` | string | The new login's password, in plain text. | +| Field | Type | Description | +|:-----------|:-------|:----------------------------------------| +| `name` | string | The new user's name. | +| `password` | string | The new user's password, in plain text. | The proposed `name` must be valid. The precise definition of valid is still up in the air, but, at minimum: @@ -143,40 +148,45 @@ The proposed `name` must be valid. The precise definition of valid is still up i -This endpoint will respond with a status of `200 Okay` when successful. The body of the response will be a JSON object describing the newly-created login: +This endpoint will respond with a status of +`200 Okay` when successful. The body of the response will be a JSON object describing the newly-created user: ```json { - "id": "Labcd1234", - "name": "Andrea" + "id": "Uabcd1234", + "name": "Andrea" } ``` The response will include the following fields: -| Field | Type | Description | -|:------------|:-------|:--| -| `id` | string | A unique identifier for the newly-created login. This can be used to associate the login with other events, or to make API calls targeting the login. | -| `name` | string | The login's name. | +| Field | Type | Description | +|:-------|:-------|:---------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | string | A unique identifier for the newly-created user. This can be used to associate the user with other events, or to make API calls targeting the user. | +| `name` | string | The user's name. | The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name. The provided password will also be converted to normalization form C. However, the normalized password is not returned to the client. -The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the login created for this request. See the [authentication](./authentication.md) section for details on how this cookie may be used. +The response will include a `Set-Cookie` header for the +`identity` cookie, providing the client with a newly-minted identity token associated with the login created for this request. See the [authentication](./authentication.md) section for details on how this cookie may be used. The cookie will expire if it is not used regularly. ### Invitation not found -This endpoint will respond with a status of `404 Not Found` when the invitation ID either does not exist, or has already been accepted. +This endpoint will respond with a status of +`404 Not Found` when the invitation ID either does not exist, or has already been accepted. ### Name not valid This endpoint will respond with a status of `400 Bad Request` if the proposed `name` is not valid. +The invitation can be accepted with a different name. + ### Name in use -This endpoint will respond with a status of `409 Conflict` if the requested login name has already been taken. +This endpoint will respond with a status of `409 Conflict` if the requested `name` has already been taken. The invitation can be accepted with a different name. -- cgit v1.2.3 From c3d774545cefd6168cfdc69c128a84bf9dee4776 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 24 Mar 2025 11:24:22 -0400 Subject: Rename `login` to `user` in the client. --- ui/lib/apiServer.js | 4 ++-- ui/lib/session.svelte.js | 10 +++++----- ui/lib/state/remote/logins.svelte.js | 18 ------------------ ui/lib/state/remote/state.svelte.js | 24 ++++++++++++------------ ui/lib/state/remote/users.svelte.js | 18 ++++++++++++++++++ 5 files changed, 37 insertions(+), 37 deletions(-) delete mode 100644 ui/lib/state/remote/logins.svelte.js create mode 100644 ui/lib/state/remote/users.svelte.js diff --git a/ui/lib/apiServer.js b/ui/lib/apiServer.js index c65b743..0b51883 100644 --- a/ui/lib/apiServer.js +++ b/ui/lib/apiServer.js @@ -45,9 +45,9 @@ export async function getInvite(inviteId) { return apiServer.get(`/invite/${inviteId}`); } -export async function acceptInvite(inviteId, username, password) { +export async function acceptInvite(inviteId, name, password) { const data = { - name: username, + name, password }; return apiServer.post(`/invite/${inviteId}`, data); diff --git a/ui/lib/session.svelte.js b/ui/lib/session.svelte.js index 16c2a98..67155ab 100644 --- a/ui/lib/session.svelte.js +++ b/ui/lib/session.svelte.js @@ -8,18 +8,18 @@ class Session { remote = $state(); local = $state(); currentUser = $derived(this.remote.currentUser); - logins = $derived(this.remote.logins.all); + users = $derived(this.remote.users.all); channels = $derived(this.remote.channels.all); messages = $derived( this.remote.messages.all.map((message) => - message.resolve({ sender: (id) => this.logins.get(id) }) + message.resolve({ sender: (id) => this.users.get(id) }) ) ); - static boot({ login, logins, channels, messages, resume_point }) { + static boot({ user, users, channels, messages, resume_point }) { const remote = r.State.boot({ - currentUser: login, - logins, + currentUser: user, + users, channels, messages, resumePoint: resume_point diff --git a/ui/lib/state/remote/logins.svelte.js b/ui/lib/state/remote/logins.svelte.js deleted file mode 100644 index d19068d..0000000 --- a/ui/lib/state/remote/logins.svelte.js +++ /dev/null @@ -1,18 +0,0 @@ -import { SvelteMap } from 'svelte/reactivity'; - -export class Logins { - all = $state(); - - static boot(logins) { - const all = new SvelteMap(logins.map((login) => [login.id, login])); - return new Logins({ all }); - } - - constructor({ all }) { - this.all = all; - } - - add({ id, name }) { - this.all.set(id, { id, name }); - } -} diff --git a/ui/lib/state/remote/state.svelte.js b/ui/lib/state/remote/state.svelte.js index c4daf17..6cbe124 100644 --- a/ui/lib/state/remote/state.svelte.js +++ b/ui/lib/state/remote/state.svelte.js @@ -1,26 +1,26 @@ -import { Logins } from './logins.svelte.js'; +import { Users } from './users.svelte.js'; import { Channels } from './channels.svelte.js'; import { Messages } from './messages.svelte.js'; export class State { currentUser = $state(); - logins = $state(); + users = $state(); channels = $state(); messages = $state(); - static boot({ currentUser, logins, channels, messages, resumePoint }) { + static boot({ currentUser, users, channels, messages, resumePoint }) { return new State({ currentUser, - logins: Logins.boot(logins), + users: Users.boot(users), channels: Channels.boot(channels), messages: Messages.boot(messages), resumePoint }); } - constructor({ currentUser, logins, channels, messages, resumePoint }) { + constructor({ currentUser, users, channels, messages, resumePoint }) { this.currentUser = currentUser; - this.logins = logins; + this.users = users; this.channels = channels; this.messages = messages; this.resumePoint = resumePoint; @@ -30,8 +30,8 @@ export class State { switch (event.type) { case 'channel': return this.onChannelEvent(event); - case 'login': - return this.onLoginEvent(event); + case 'user': + return this.onUserEvent(event); case 'message': return this.onMessageEvent(event); } @@ -56,16 +56,16 @@ export class State { this.channels.remove(id); } - onLoginEvent(event) { + onUserEvent(event) { switch (event.event) { case 'created': - return this.onLoginCreated(event); + return this.onUserCreated(event); } } - onLoginCreated(event) { + onUserCreated(event) { const { id, name } = event; - this.logins.add({ id, name }); + this.users.add({ id, name }); } onMessageEvent(event) { diff --git a/ui/lib/state/remote/users.svelte.js b/ui/lib/state/remote/users.svelte.js new file mode 100644 index 0000000..617084f --- /dev/null +++ b/ui/lib/state/remote/users.svelte.js @@ -0,0 +1,18 @@ +import { SvelteMap } from 'svelte/reactivity'; + +export class Users { + all = $state(); + + static boot(users) { + const all = new SvelteMap(users.map((user) => [user.id, user])); + return new Users({ all }); + } + + constructor({ all }) { + this.all = all; + } + + add({ id, name }) { + this.all.set(id, { id, name }); + } +} -- cgit v1.2.3 From 45eea07a56022f647b3a273798a5255cda73f13d Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 24 Mar 2025 23:33:36 -0400 Subject: Rename a bunch of straggler references to `login`. --- src/boot/app.rs | 8 ++++---- src/setup/app.rs | 4 ++-- src/setup/routes/post.rs | 4 ++-- src/test/fixtures/identity.rs | 8 ++++---- src/test/fixtures/user.rs | 4 ++-- src/token/app.rs | 26 +++++++++++++------------- src/token/repo/auth.rs | 8 ++++---- src/token/repo/token.rs | 20 ++++++++++---------- 8 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/boot/app.rs b/src/boot/app.rs index 264d5ae..f531afe 100644 --- a/src/boot/app.rs +++ b/src/boot/app.rs @@ -22,15 +22,15 @@ impl<'a> Boot<'a> { let mut tx = self.db.begin().await?; let resume_point = tx.sequence().current().await?; - let logins = tx.users().all(resume_point).await?; + let users = tx.users().all(resume_point).await?; let channels = tx.channels().all(resume_point).await?; let messages = tx.messages().all(resume_point).await?; tx.commit().await?; - let logins = logins + let users = users .into_iter() - .filter_map(|login| login.as_of(resume_point)) + .filter_map(|user| user.as_of(resume_point)) .collect(); let channels = channels @@ -45,7 +45,7 @@ impl<'a> Boot<'a> { Ok(Snapshot { resume_point, - users: logins, + users, channels, messages, }) diff --git a/src/setup/app.rs b/src/setup/app.rs index 9f31c01..26eed7a 100644 --- a/src/setup/app.rs +++ b/src/setup/app.rs @@ -41,9 +41,9 @@ impl<'a> Setup<'a> { let secret = tx.tokens().issue(stored.user(), created_at).await?; tx.commit().await?; - let login = stored.publish(self.events); + let user = stored.publish(self.events); - Ok((login.as_created(), secret)) + Ok((user.as_created(), secret)) } pub async fn completed(&self) -> Result { diff --git a/src/setup/routes/post.rs b/src/setup/routes/post.rs index 9c6b7a6..0ff5d69 100644 --- a/src/setup/routes/post.rs +++ b/src/setup/routes/post.rs @@ -20,13 +20,13 @@ pub async fn handler( identity: IdentityCookie, Json(request): Json, ) -> Result<(IdentityCookie, Json), Error> { - let (login, secret) = app + let (user, secret) = app .setup() .initial(&request.name, &request.password, &setup_at) .await .map_err(Error)?; let identity = identity.set(secret); - Ok((identity, Json(login))) + Ok((identity, Json(user))) } #[derive(serde::Deserialize)] diff --git a/src/test/fixtures/identity.rs b/src/test/fixtures/identity.rs index 0b2f978..cb325d8 100644 --- a/src/test/fixtures/identity.rs +++ b/src/test/fixtures/identity.rs @@ -21,13 +21,13 @@ pub async fn from_cookie( validated_at: &RequestedAt, ) -> Identity { let secret = cookie.secret().expect("identity token has a secret"); - let (token, login) = app + let (token, user) = app .tokens() .validate(&secret, validated_at) .await .expect("always validates newly-issued secret"); - Identity { token, user: login } + Identity { token, user } } pub async fn logged_in( @@ -41,7 +41,7 @@ pub async fn logged_in( pub fn fictitious() -> Identity { let token = token::Id::generate(); - let login = fixtures::user::fictitious(); + let user = fixtures::user::fictitious(); - Identity { token, user: login } + Identity { token, user } } diff --git a/src/test/fixtures/user.rs b/src/test/fixtures/user.rs index e668c95..6448f64 100644 --- a/src/test/fixtures/user.rs +++ b/src/test/fixtures/user.rs @@ -10,13 +10,13 @@ use crate::{ pub async fn create_with_password(app: &App, created_at: &RequestedAt) -> (Name, Password) { let (name, password) = propose(); - let login = app + let user = app .users() .create(&name, &password, created_at) .await .expect("should always succeed if the login is actually new"); - (login.name, password) + (user.name, password) } pub async fn create(app: &App, created_at: &RequestedAt) -> User { diff --git a/src/token/app.rs b/src/token/app.rs index 211df81..46b2d73 100644 --- a/src/token/app.rs +++ b/src/token/app.rs @@ -33,7 +33,7 @@ impl<'a> Tokens<'a> { login_at: &DateTime, ) -> Result<(User, Secret), LoginError> { let mut tx = self.db.begin().await?; - let (login, stored_hash) = tx + let (user, stored_hash) = tx .auth() .for_name(name) .await @@ -46,11 +46,11 @@ impl<'a> Tokens<'a> { // if the account is deleted during that time. tx.commit().await?; - let snapshot = login.as_snapshot().ok_or(LoginError::Rejected)?; + let snapshot = user.as_snapshot().ok_or(LoginError::Rejected)?; let token = if stored_hash.verify(password)? { let mut tx = self.db.begin().await?; - let token = tx.tokens().issue(&login, login_at).await?; + let token = tx.tokens().issue(&user, login_at).await?; tx.commit().await?; token } else { @@ -62,15 +62,15 @@ impl<'a> Tokens<'a> { pub async fn change_password( &self, - login: &User, + user: &User, password: &Password, to: &Password, changed_at: &DateTime, ) -> Result<(User, Secret), LoginError> { let mut tx = self.db.begin().await?; - let (login, stored_hash) = tx + let (user, stored_hash) = tx .auth() - .for_login(login) + .for_user(user) .await .optional()? .ok_or(LoginError::Rejected)?; @@ -85,13 +85,13 @@ impl<'a> Tokens<'a> { return Err(LoginError::Rejected); } - let snapshot = login.as_snapshot().ok_or(LoginError::Rejected)?; + let snapshot = user.as_snapshot().ok_or(LoginError::Rejected)?; let to_hash = to.hash()?; let mut tx = self.db.begin().await?; - let tokens = tx.tokens().revoke_all(&login).await?; - tx.users().set_password(&login, &to_hash).await?; - let secret = tx.tokens().issue(&login, changed_at).await?; + let tokens = tx.tokens().revoke_all(&user).await?; + tx.users().set_password(&user, &to_hash).await?; + let secret = tx.tokens().issue(&user, changed_at).await?; tx.commit().await?; for event in tokens.into_iter().map(TokenEvent::Revoked) { @@ -107,16 +107,16 @@ impl<'a> Tokens<'a> { used_at: &DateTime, ) -> Result<(Id, User), ValidateError> { let mut tx = self.db.begin().await?; - let (token, login) = tx + let (token, user) = tx .tokens() .validate(secret, used_at) .await .not_found(|| ValidateError::InvalidToken)?; tx.commit().await?; - let login = login.as_snapshot().ok_or(ValidateError::LoginDeleted)?; + let user = user.as_snapshot().ok_or(ValidateError::LoginDeleted)?; - Ok((token, login)) + Ok((token, user)) } pub async fn limit_stream( diff --git a/src/token/repo/auth.rs b/src/token/repo/auth.rs index a1f4aad..68a81c7 100644 --- a/src/token/repo/auth.rs +++ b/src/token/repo/auth.rs @@ -51,7 +51,7 @@ impl Auth<'_> { Ok((login, row.password_hash)) } - pub async fn for_login(&mut self, login: &User) -> Result<(History, StoredHash), LoadError> { + pub async fn for_user(&mut self, user: &User) -> Result<(History, StoredHash), LoadError> { let row = sqlx::query!( r#" select @@ -64,12 +64,12 @@ impl Auth<'_> { from user where id = $1 "#, - login.id, + user.id, ) .fetch_one(&mut *self.0) .await?; - let login = History { + let user = History { user: User { id: row.id, name: Name::new(row.display_name, row.canonical_name)?, @@ -77,7 +77,7 @@ impl Auth<'_> { created: Instant::new(row.created_at, row.created_sequence), }; - Ok((login, row.password_hash)) + Ok((user, row.password_hash)) } } diff --git a/src/token/repo/token.rs b/src/token/repo/token.rs index 145ba2d..e49c2d4 100644 --- a/src/token/repo/token.rs +++ b/src/token/repo/token.rs @@ -27,12 +27,12 @@ impl Tokens<'_> { // be used to control expiry, until the token is actually used. pub async fn issue( &mut self, - login: &History, + user: &History, issued_at: &DateTime, ) -> Result { let id = Id::generate(); let secret = Uuid::new_v4().to_string(); - let login = login.id(); + let user = user.id(); let secret = sqlx::query_scalar!( r#" @@ -43,7 +43,7 @@ impl Tokens<'_> { "#, id, secret, - login, + user, issued_at, ) .fetch_one(&mut *self.0) @@ -85,8 +85,8 @@ impl Tokens<'_> { } // Revoke tokens for a login - pub async fn revoke_all(&mut self, login: &user::History) -> Result, sqlx::Error> { - let login = login.id(); + pub async fn revoke_all(&mut self, user: &user::History) -> Result, sqlx::Error> { + let user = user.id(); let tokens = sqlx::query_scalar!( r#" delete @@ -94,7 +94,7 @@ impl Tokens<'_> { where user = $1 returning id as "id: Id" "#, - login, + user, ) .fetch_all(&mut *self.0) .await?; @@ -132,7 +132,7 @@ impl Tokens<'_> { // sqlite3, as of this writing, does not allow an update's `returning` // clause to reference columns from tables joined into the update. Two // queries is fine, but it feels untidy. - let (token, login) = sqlx::query!( + let (token, user) = sqlx::query!( r#" update token set last_used_at = $1 @@ -148,7 +148,7 @@ impl Tokens<'_> { .fetch_one(&mut *self.0) .await?; - let login = sqlx::query!( + let user = sqlx::query!( r#" select id as "id: user::Id", @@ -159,7 +159,7 @@ impl Tokens<'_> { from user where id = $1 "#, - login, + user, ) .map(|row| { Ok::<_, name::Error>(History { @@ -173,7 +173,7 @@ impl Tokens<'_> { .fetch_one(&mut *self.0) .await??; - Ok((token, login)) + Ok((token, user)) } } -- cgit v1.2.3