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. --- src/token/repo/auth.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/token/repo/auth.rs') 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, -- 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 (limited to 'src/token/repo/auth.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 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(-) (limited to 'src/token/repo/auth.rs') 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