diff options
| -rw-r--r-- | .sqlx/query-0caf898537dd38eea00322a9eb3578281dc70941faf05d53f8525034446ae52f.json (renamed from .sqlx/query-8d9b83fb53e4191ee58b68e65e101d0941f2a266679209f793fd27466b9e3719.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-0f0e4a6ac32b39f3bd7f4832389259b91bbffa182e32b224635031eead2fa82d.json (renamed from .sqlx/query-836a37fa3fbcacbc880c7c9e603b3087a17906db95d14a35034889a253f23418.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-24bc0257eff3357322481e1314f70d13e8b0ca22b7652f1063ec7796cf307269.json (renamed from .sqlx/query-400de8e1dca27c48362b060c5b16e9ae908c4ea30e2d682d59d4720699b8de91.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-2e0aa3126267465ee1ae01e6856eff74a544f0a1c3692766e48a3182df5ada98.json (renamed from .sqlx/query-34d3bc2b18bc0813cbaafc1ef99603f820d3b391f212c2c0883506a53ee757ca.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-40e0310af2814435cca882701b86eddb5a1114202ba385f66f131b1601feec11.json (renamed from .sqlx/query-e702a0e7ff9a0a9808f8d45294ae908114b03719dc0cb237cec11f807bf757b1.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-4392eb4ec7257676acdce461b3bd7892ca01c2e3a2e0b1abfd8d7a57cbbf265e.json (renamed from .sqlx/query-ae9d1c997891f7e917cde554a251e76e93d3a43d1447faa4ec4085f7b3f60404.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-48884b1c153cbf6e10eed72e34ec512dfc9109b338eea475c209e5a005ae20c6.json (renamed from .sqlx/query-152cbd56cdce2ee7e94ae2568c112d0f6fe2aa136f0758a68f1eb66787db52f0.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-78d24fa907f3dcc0c129880e83b4ef41bad03b57937a27f98aebbef5268ef5f5.json (renamed from .sqlx/query-8e816ccef8ecee937d88ff31ca611395405093dbbed738579d715b2d623921be.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-7f5bbd935941210ba2f25e65327d32a68f41258a27249027f9200b2bbba047cb.json (renamed from .sqlx/query-647c3387fd01747b6318b946d2ea19a3007558abe81a1635c64ef7960acd30b2.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-91df35ca0c610b05fb353eeb7b7ef2f4fca0cd1c43ef45f32c9ce069d37fe659.json (renamed from .sqlx/query-b09438f4b1247a4e3991751de1fa77f2a18537d30e61ccbdcc947d0dba2b3da3.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-be33d1fb2f71093ed73efd90c8d4dfe599c70b36607a5dc436f28ba5b2ea9b2e.json (renamed from .sqlx/query-b991b34b491306780a1b6efa157b6ee50f32e1136ad9cbd91caa0add2ab3cdaa.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-d693a55bf9394ea79a892c3a5ed7d651ce7c5b3c7e8960458af03f1b533e3b1f.json (renamed from .sqlx/query-3fbec32aeb32c49e088f246c00151035dcf174cec137326b63e0cb0e4ae5cb60.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-db77e97937167d1edbbe88ebb2c0efd1c5718e721fe906765c28d040e446acd7.json (renamed from .sqlx/query-9da5b746de5d481eeb0755283b4094fa9dfa65dc95991f689355889606ab1c46.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-dbbc785bc45173db773f9179ae0758568f732d837c923cfe1b142181fb5d83f3.json (renamed from .sqlx/query-4224d5c1c4009e0d31b96bc7b1d9f6a2215c7c135720c1222170a1f6692c3a8a.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-dd613246fc1e87039f0a12ca8e2fa7c9cee66d2dfc3e516064982609cdcb3ff6.json (renamed from .sqlx/query-e2686f26f8646b4cd31beeb2060b9a6d6e0bbcb4cf8d01c48b297e6f0a950ebc.json) | 6 | ||||
| -rw-r--r-- | .sqlx/query-fce8f4fbd59a8b3b8531e10599914331682d58ace57214bfa26ccaa089592a24.json (renamed from .sqlx/query-bddc3b0d75f6048c36630db3abb8945a49ce18fb715d249bc9d93fc7d10e817d.json) | 6 | ||||
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | docs/api/channels-messages.md | 4 | ||||
| -rw-r--r-- | docs/api/initial-setup.md | 4 | ||||
| -rw-r--r-- | docs/api/invitations.md | 4 | ||||
| -rw-r--r-- | src/channel/app.rs | 8 | ||||
| -rw-r--r-- | src/channel/mod.rs | 5 | ||||
| -rw-r--r-- | src/channel/name.rs | 30 | ||||
| -rw-r--r-- | src/channel/repo.rs | 14 | ||||
| -rw-r--r-- | src/channel/routes/channel/post.rs | 4 | ||||
| -rw-r--r-- | src/channel/routes/post.rs | 4 | ||||
| -rw-r--r-- | src/channel/snapshot.rs | 4 | ||||
| -rw-r--r-- | src/invite/app.rs | 8 | ||||
| -rw-r--r-- | src/invite/routes/invite/post.rs | 4 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/login/app.rs | 4 | ||||
| -rw-r--r-- | src/login/mod.rs | 4 | ||||
| -rw-r--r-- | src/login/name.rs | 28 | ||||
| -rw-r--r-- | src/login/password.rs | 7 | ||||
| -rw-r--r-- | src/login/repo.rs | 10 | ||||
| -rw-r--r-- | src/login/routes/login/post.rs | 4 | ||||
| -rw-r--r-- | src/login/snapshot.rs | 4 | ||||
| -rw-r--r-- | src/message/app.rs | 4 | ||||
| -rw-r--r-- | src/message/body.rs | 30 | ||||
| -rw-r--r-- | src/message/mod.rs | 5 | ||||
| -rw-r--r-- | src/message/repo.rs | 28 | ||||
| -rw-r--r-- | src/message/snapshot.rs | 4 | ||||
| -rw-r--r-- | src/nfc.rs | 103 | ||||
| -rw-r--r-- | src/setup/app.rs | 4 | ||||
| -rw-r--r-- | src/setup/routes/post.rs | 4 | ||||
| -rw-r--r-- | src/test/fixtures/channel.rs | 10 | ||||
| -rw-r--r-- | src/test/fixtures/login.rs | 12 | ||||
| -rw-r--r-- | src/test/fixtures/message.rs | 6 | ||||
| -rw-r--r-- | src/token/app.rs | 4 | ||||
| -rw-r--r-- | src/token/repo/auth.rs | 6 | ||||
| -rw-r--r-- | src/token/repo/token.rs | 4 |
52 files changed, 346 insertions, 131 deletions
diff --git a/.sqlx/query-8d9b83fb53e4191ee58b68e65e101d0941f2a266679209f793fd27466b9e3719.json b/.sqlx/query-0caf898537dd38eea00322a9eb3578281dc70941faf05d53f8525034446ae52f.json index 4d28006..8990436 100644 --- a/.sqlx/query-8d9b83fb53e4191ee58b68e65e101d0941f2a266679209f793fd27466b9e3719.json +++ b/.sqlx/query-0caf898537dd38eea00322a9eb3578281dc70941faf05d53f8525034446ae52f.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n select\n id as \"id: Id\",\n channel.name,\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n where coalesce(channel.created_sequence <= $1, true)\n order by channel.name\n ", + "query": "\n select\n id as \"id: Id\",\n channel.name as \"name: Name\",\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n where coalesce(channel.created_sequence <= $1, true)\n order by channel.name\n ", "describe": { "columns": [ { @@ -9,7 +9,7 @@ "type_info": "Text" }, { - "name": "name", + "name": "name: Name", "ordinal": 1, "type_info": "Text" }, @@ -46,5 +46,5 @@ true ] }, - "hash": "8d9b83fb53e4191ee58b68e65e101d0941f2a266679209f793fd27466b9e3719" + "hash": "0caf898537dd38eea00322a9eb3578281dc70941faf05d53f8525034446ae52f" } diff --git a/.sqlx/query-836a37fa3fbcacbc880c7c9e603b3087a17906db95d14a35034889a253f23418.json b/.sqlx/query-0f0e4a6ac32b39f3bd7f4832389259b91bbffa182e32b224635031eead2fa82d.json index 4770b7e..fd5a165 100644 --- a/.sqlx/query-836a37fa3fbcacbc880c7c9e603b3087a17906db95d14a35034889a253f23418.json +++ b/.sqlx/query-0f0e4a6ac32b39f3bd7f4832389259b91bbffa182e32b224635031eead2fa82d.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\t\t\tinsert into message\n\t\t\t\t\t(id, channel, sender, sent_at, sent_sequence, body)\n\t\t\t\tvalues ($1, $2, $3, $4, $5, $6)\n\t\t\t\treturning\n\t\t\t\t\tid 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\t\t\t\t\tbody\n\t\t\t", + "query": "\n insert into message\n (id, channel, sender, sent_at, sent_sequence, body)\n values ($1, $2, $3, $4, $5, $6)\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": [ { @@ -29,7 +29,7 @@ "type_info": "Integer" }, { - "name": "body", + "name": "body: Body", "ordinal": 5, "type_info": "Text" } @@ -46,5 +46,5 @@ true ] }, - "hash": "836a37fa3fbcacbc880c7c9e603b3087a17906db95d14a35034889a253f23418" + "hash": "0f0e4a6ac32b39f3bd7f4832389259b91bbffa182e32b224635031eead2fa82d" } diff --git a/.sqlx/query-400de8e1dca27c48362b060c5b16e9ae908c4ea30e2d682d59d4720699b8de91.json b/.sqlx/query-24bc0257eff3357322481e1314f70d13e8b0ca22b7652f1063ec7796cf307269.json index 0caa950..9f09a28 100644 --- a/.sqlx/query-400de8e1dca27c48362b060c5b16e9ae908c4ea30e2d682d59d4720699b8de91.json +++ b/.sqlx/query-24bc0257eff3357322481e1314f70d13e8b0ca22b7652f1063ec7796cf307269.json @@ -1,6 +1,6 @@ { "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,\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 coalesce(message.sent_sequence > $1, true)\n ", + "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 coalesce(message.sent_sequence > $1, true)\n ", "describe": { "columns": [ { @@ -29,7 +29,7 @@ "type_info": "Integer" }, { - "name": "body", + "name": "body: Body", "ordinal": 5, "type_info": "Text" }, @@ -58,5 +58,5 @@ true ] }, - "hash": "400de8e1dca27c48362b060c5b16e9ae908c4ea30e2d682d59d4720699b8de91" + "hash": "24bc0257eff3357322481e1314f70d13e8b0ca22b7652f1063ec7796cf307269" } diff --git a/.sqlx/query-34d3bc2b18bc0813cbaafc1ef99603f820d3b391f212c2c0883506a53ee757ca.json b/.sqlx/query-2e0aa3126267465ee1ae01e6856eff74a544f0a1c3692766e48a3182df5ada98.json index 63ff012..227d242 100644 --- a/.sqlx/query-34d3bc2b18bc0813cbaafc1ef99603f820d3b391f212c2c0883506a53ee757ca.json +++ b/.sqlx/query-2e0aa3126267465ee1ae01e6856eff74a544f0a1c3692766e48a3182df5ada98.json @@ -1,6 +1,6 @@ { "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,\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 ", + "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": [ { @@ -29,7 +29,7 @@ "type_info": "Integer" }, { - "name": "body", + "name": "body: Body", "ordinal": 5, "type_info": "Text" }, @@ -58,5 +58,5 @@ false ] }, - "hash": "34d3bc2b18bc0813cbaafc1ef99603f820d3b391f212c2c0883506a53ee757ca" + "hash": "2e0aa3126267465ee1ae01e6856eff74a544f0a1c3692766e48a3182df5ada98" } diff --git a/.sqlx/query-e702a0e7ff9a0a9808f8d45294ae908114b03719dc0cb237cec11f807bf757b1.json b/.sqlx/query-40e0310af2814435cca882701b86eddb5a1114202ba385f66f131b1601feec11.json index ba35bb9..3147d7f 100644 --- a/.sqlx/query-e702a0e7ff9a0a9808f8d45294ae908114b03719dc0cb237cec11f807bf757b1.json +++ b/.sqlx/query-40e0310af2814435cca882701b86eddb5a1114202ba385f66f131b1601feec11.json @@ -1,6 +1,6 @@ { "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,\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 ", + "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": [ { @@ -19,7 +19,7 @@ "type_info": "Text" }, { - "name": "body", + "name": "body: Body", "ordinal": 3, "type_info": "Text" }, @@ -58,5 +58,5 @@ true ] }, - "hash": "e702a0e7ff9a0a9808f8d45294ae908114b03719dc0cb237cec11f807bf757b1" + "hash": "40e0310af2814435cca882701b86eddb5a1114202ba385f66f131b1601feec11" } diff --git a/.sqlx/query-ae9d1c997891f7e917cde554a251e76e93d3a43d1447faa4ec4085f7b3f60404.json b/.sqlx/query-4392eb4ec7257676acdce461b3bd7892ca01c2e3a2e0b1abfd8d7a57cbbf265e.json index cb345dc..1dd685b 100644 --- a/.sqlx/query-ae9d1c997891f7e917cde554a251e76e93d3a43d1447faa4ec4085f7b3f60404.json +++ b/.sqlx/query-4392eb4ec7257676acdce461b3bd7892ca01c2e3a2e0b1abfd8d7a57cbbf265e.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n select\n token.id as \"token_id: Id\",\n login.id as \"login_id: login::Id\",\n login.name as \"login_name\"\n from login\n join token on login.id = token.login\n where token.secret = $1\n ", + "query": "\n select\n token.id as \"token_id: Id\",\n login.id as \"login_id: login::Id\",\n login.name as \"login_name: Name\"\n from login\n join token on login.id = token.login\n where token.secret = $1\n ", "describe": { "columns": [ { @@ -14,7 +14,7 @@ "type_info": "Text" }, { - "name": "login_name", + "name": "login_name: Name", "ordinal": 2, "type_info": "Text" } @@ -28,5 +28,5 @@ false ] }, - "hash": "ae9d1c997891f7e917cde554a251e76e93d3a43d1447faa4ec4085f7b3f60404" + "hash": "4392eb4ec7257676acdce461b3bd7892ca01c2e3a2e0b1abfd8d7a57cbbf265e" } diff --git a/.sqlx/query-152cbd56cdce2ee7e94ae2568c112d0f6fe2aa136f0758a68f1eb66787db52f0.json b/.sqlx/query-48884b1c153cbf6e10eed72e34ec512dfc9109b338eea475c209e5a005ae20c6.json index 2c7666f..b8d7303 100644 --- a/.sqlx/query-152cbd56cdce2ee7e94ae2568c112d0f6fe2aa136f0758a68f1eb66787db52f0.json +++ b/.sqlx/query-48884b1c153cbf6e10eed72e34ec512dfc9109b338eea475c209e5a005ae20c6.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n select\n id as \"id: Id\",\n channel.name,\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n where id = $1\n ", + "query": "\n select\n id as \"id: Id\",\n channel.name as \"name: Name\",\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n where id = $1\n ", "describe": { "columns": [ { @@ -9,7 +9,7 @@ "type_info": "Text" }, { - "name": "name", + "name": "name: Name", "ordinal": 1, "type_info": "Text" }, @@ -46,5 +46,5 @@ false ] }, - "hash": "152cbd56cdce2ee7e94ae2568c112d0f6fe2aa136f0758a68f1eb66787db52f0" + "hash": "48884b1c153cbf6e10eed72e34ec512dfc9109b338eea475c209e5a005ae20c6" } diff --git a/.sqlx/query-8e816ccef8ecee937d88ff31ca611395405093dbbed738579d715b2d623921be.json b/.sqlx/query-78d24fa907f3dcc0c129880e83b4ef41bad03b57937a27f98aebbef5268ef5f5.json index 5a038ac..09440ca 100644 --- a/.sqlx/query-8e816ccef8ecee937d88ff31ca611395405093dbbed738579d715b2d623921be.json +++ b/.sqlx/query-78d24fa907f3dcc0c129880e83b4ef41bad03b57937a27f98aebbef5268ef5f5.json @@ -1,6 +1,6 @@ { "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,\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 ", + "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": [ { @@ -19,7 +19,7 @@ "type_info": "Text" }, { - "name": "body", + "name": "body: Body", "ordinal": 3, "type_info": "Text" }, @@ -58,5 +58,5 @@ false ] }, - "hash": "8e816ccef8ecee937d88ff31ca611395405093dbbed738579d715b2d623921be" + "hash": "78d24fa907f3dcc0c129880e83b4ef41bad03b57937a27f98aebbef5268ef5f5" } diff --git a/.sqlx/query-647c3387fd01747b6318b946d2ea19a3007558abe81a1635c64ef7960acd30b2.json b/.sqlx/query-7f5bbd935941210ba2f25e65327d32a68f41258a27249027f9200b2bbba047cb.json index f706f81..83e730d 100644 --- a/.sqlx/query-647c3387fd01747b6318b946d2ea19a3007558abe81a1635c64ef7960acd30b2.json +++ b/.sqlx/query-7f5bbd935941210ba2f25e65327d32a68f41258a27249027f9200b2bbba047cb.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n select\n channel.id as \"id: Id\",\n channel.name,\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n left join message\n where channel.created_at < $1\n and message.id is null\n and deleted.id is null\n ", + "query": "\n select\n channel.id as \"id: Id\",\n channel.name as \"name: Name\",\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at?: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence?: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n left join message\n where channel.created_at < $1\n and message.id is null\n and deleted.id is null\n ", "describe": { "columns": [ { @@ -9,7 +9,7 @@ "type_info": "Text" }, { - "name": "name", + "name": "name: Name", "ordinal": 1, "type_info": "Text" }, @@ -46,5 +46,5 @@ false ] }, - "hash": "647c3387fd01747b6318b946d2ea19a3007558abe81a1635c64ef7960acd30b2" + "hash": "7f5bbd935941210ba2f25e65327d32a68f41258a27249027f9200b2bbba047cb" } diff --git a/.sqlx/query-b09438f4b1247a4e3991751de1fa77f2a18537d30e61ccbdcc947d0dba2b3da3.json b/.sqlx/query-91df35ca0c610b05fb353eeb7b7ef2f4fca0cd1c43ef45f32c9ce069d37fe659.json index 7c83aa1..6d2cb52 100644 --- a/.sqlx/query-b09438f4b1247a4e3991751de1fa77f2a18537d30e61ccbdcc947d0dba2b3da3.json +++ b/.sqlx/query-91df35ca0c610b05fb353eeb7b7ef2f4fca0cd1c43ef45f32c9ce069d37fe659.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n select\n id as \"id: Id\",\n name,\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from login\n where coalesce(created_sequence <= $1, true)\n order by created_sequence\n ", + "query": "\n select\n id as \"id: Id\",\n name as \"name: Name\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from login\n where coalesce(created_sequence <= $1, true)\n order by created_sequence\n ", "describe": { "columns": [ { @@ -9,7 +9,7 @@ "type_info": "Text" }, { - "name": "name", + "name": "name: Name", "ordinal": 1, "type_info": "Text" }, @@ -34,5 +34,5 @@ false ] }, - "hash": "b09438f4b1247a4e3991751de1fa77f2a18537d30e61ccbdcc947d0dba2b3da3" + "hash": "91df35ca0c610b05fb353eeb7b7ef2f4fca0cd1c43ef45f32c9ce069d37fe659" } diff --git a/.sqlx/query-b991b34b491306780a1b6efa157b6ee50f32e1136ad9cbd91caa0add2ab3cdaa.json b/.sqlx/query-be33d1fb2f71093ed73efd90c8d4dfe599c70b36607a5dc436f28ba5b2ea9b2e.json index 3901207..4b69943 100644 --- a/.sqlx/query-b991b34b491306780a1b6efa157b6ee50f32e1136ad9cbd91caa0add2ab3cdaa.json +++ b/.sqlx/query-be33d1fb2f71093ed73efd90c8d4dfe599c70b36607a5dc436f28ba5b2ea9b2e.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n\t\t\t\tselect\n\t\t\t\t\tid as \"id: login::Id\",\n\t\t\t\t\tname,\n\t\t\t\t\tpassword_hash as \"password_hash: StoredHash\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n\t\t\t\tfrom login\n\t\t\t\twhere name = $1\n\t\t\t", + "query": "\n\t\t\t\tselect\n\t\t\t\t\tid as \"id: login::Id\",\n\t\t\t\t\tname as \"name: Name\",\n\t\t\t\t\tpassword_hash as \"password_hash: StoredHash\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n\t\t\t\tfrom login\n\t\t\t\twhere name = $1\n\t\t\t", "describe": { "columns": [ { @@ -9,7 +9,7 @@ "type_info": "Text" }, { - "name": "name", + "name": "name: Name", "ordinal": 1, "type_info": "Text" }, @@ -40,5 +40,5 @@ false ] }, - "hash": "b991b34b491306780a1b6efa157b6ee50f32e1136ad9cbd91caa0add2ab3cdaa" + "hash": "be33d1fb2f71093ed73efd90c8d4dfe599c70b36607a5dc436f28ba5b2ea9b2e" } diff --git a/.sqlx/query-3fbec32aeb32c49e088f246c00151035dcf174cec137326b63e0cb0e4ae5cb60.json b/.sqlx/query-d693a55bf9394ea79a892c3a5ed7d651ce7c5b3c7e8960458af03f1b533e3b1f.json index 01e72b2..4291236 100644 --- a/.sqlx/query-3fbec32aeb32c49e088f246c00151035dcf174cec137326b63e0cb0e4ae5cb60.json +++ b/.sqlx/query-d693a55bf9394ea79a892c3a5ed7d651ce7c5b3c7e8960458af03f1b533e3b1f.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n insert\n into login (id, name, password_hash, created_sequence, created_at)\n values ($1, $2, $3, $4, $5)\n returning\n id as \"id: Id\",\n name,\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n ", + "query": "\n insert\n into login (id, name, password_hash, created_sequence, created_at)\n values ($1, $2, $3, $4, $5)\n returning\n id as \"id: Id\",\n name as \"name: Name\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n ", "describe": { "columns": [ { @@ -9,7 +9,7 @@ "type_info": "Text" }, { - "name": "name", + "name": "name: Name", "ordinal": 1, "type_info": "Text" }, @@ -34,5 +34,5 @@ false ] }, - "hash": "3fbec32aeb32c49e088f246c00151035dcf174cec137326b63e0cb0e4ae5cb60" + "hash": "d693a55bf9394ea79a892c3a5ed7d651ce7c5b3c7e8960458af03f1b533e3b1f" } diff --git a/.sqlx/query-9da5b746de5d481eeb0755283b4094fa9dfa65dc95991f689355889606ab1c46.json b/.sqlx/query-db77e97937167d1edbbe88ebb2c0efd1c5718e721fe906765c28d040e446acd7.json index 56a8498..0f2045f 100644 --- a/.sqlx/query-9da5b746de5d481eeb0755283b4094fa9dfa65dc95991f689355889606ab1c46.json +++ b/.sqlx/query-db77e97937167d1edbbe88ebb2c0efd1c5718e721fe906765c28d040e446acd7.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n insert\n into channel (id, name, created_at, created_sequence)\n values ($1, $2, $3, $4)\n returning\n id as \"id: Id\",\n name as \"name!\", -- known non-null as we just set it\n created_at as \"created_at: DateTime\",\n created_sequence as \"created_sequence: Sequence\"\n ", + "query": "\n insert\n into channel (id, name, created_at, created_sequence)\n values ($1, $2, $3, $4)\n returning\n id as \"id: Id\",\n name as \"name!: Name\", -- known non-null as we just set it\n created_at as \"created_at: DateTime\",\n created_sequence as \"created_sequence: Sequence\"\n ", "describe": { "columns": [ { @@ -9,7 +9,7 @@ "type_info": "Text" }, { - "name": "name!", + "name": "name!: Name", "ordinal": 1, "type_info": "Text" }, @@ -34,5 +34,5 @@ false ] }, - "hash": "9da5b746de5d481eeb0755283b4094fa9dfa65dc95991f689355889606ab1c46" + "hash": "db77e97937167d1edbbe88ebb2c0efd1c5718e721fe906765c28d040e446acd7" } diff --git a/.sqlx/query-4224d5c1c4009e0d31b96bc7b1d9f6a2215c7c135720c1222170a1f6692c3a8a.json b/.sqlx/query-dbbc785bc45173db773f9179ae0758568f732d837c923cfe1b142181fb5d83f3.json index 767c217..7b5ac51 100644 --- a/.sqlx/query-4224d5c1c4009e0d31b96bc7b1d9f6a2215c7c135720c1222170a1f6692c3a8a.json +++ b/.sqlx/query-dbbc785bc45173db773f9179ae0758568f732d837c923cfe1b142181fb5d83f3.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n select\n id as \"id: Id\",\n name,\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from login\n where coalesce(login.created_sequence > $1, true)\n ", + "query": "\n select\n id as \"id: Id\",\n name as \"name: Name\",\n created_sequence as \"created_sequence: Sequence\",\n created_at as \"created_at: DateTime\"\n from login\n where coalesce(login.created_sequence > $1, true)\n ", "describe": { "columns": [ { @@ -9,7 +9,7 @@ "type_info": "Text" }, { - "name": "name", + "name": "name: Name", "ordinal": 1, "type_info": "Text" }, @@ -34,5 +34,5 @@ false ] }, - "hash": "4224d5c1c4009e0d31b96bc7b1d9f6a2215c7c135720c1222170a1f6692c3a8a" + "hash": "dbbc785bc45173db773f9179ae0758568f732d837c923cfe1b142181fb5d83f3" } diff --git a/.sqlx/query-e2686f26f8646b4cd31beeb2060b9a6d6e0bbcb4cf8d01c48b297e6f0a950ebc.json b/.sqlx/query-dd613246fc1e87039f0a12ca8e2fa7c9cee66d2dfc3e516064982609cdcb3ff6.json index 1883035..fe8be3f 100644 --- a/.sqlx/query-e2686f26f8646b4cd31beeb2060b9a6d6e0bbcb4cf8d01c48b297e6f0a950ebc.json +++ b/.sqlx/query-dd613246fc1e87039f0a12ca8e2fa7c9cee66d2dfc3e516064982609cdcb3ff6.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n select\n id as \"id: Id\",\n channel.name,\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n where coalesce(channel.created_sequence > $1, true)\n ", + "query": "\n select\n id as \"id: Id\",\n channel.name as \"name: Name\",\n channel.created_at as \"created_at: DateTime\",\n channel.created_sequence as \"created_sequence: Sequence\",\n deleted.deleted_at as \"deleted_at: DateTime\",\n deleted.deleted_sequence as \"deleted_sequence: Sequence\"\n from channel\n left join channel_deleted as deleted\n using (id)\n where coalesce(channel.created_sequence > $1, true)\n ", "describe": { "columns": [ { @@ -9,7 +9,7 @@ "type_info": "Text" }, { - "name": "name", + "name": "name: Name", "ordinal": 1, "type_info": "Text" }, @@ -46,5 +46,5 @@ true ] }, - "hash": "e2686f26f8646b4cd31beeb2060b9a6d6e0bbcb4cf8d01c48b297e6f0a950ebc" + "hash": "dd613246fc1e87039f0a12ca8e2fa7c9cee66d2dfc3e516064982609cdcb3ff6" } diff --git a/.sqlx/query-bddc3b0d75f6048c36630db3abb8945a49ce18fb715d249bc9d93fc7d10e817d.json b/.sqlx/query-fce8f4fbd59a8b3b8531e10599914331682d58ace57214bfa26ccaa089592a24.json index abc1851..7aab764 100644 --- a/.sqlx/query-bddc3b0d75f6048c36630db3abb8945a49ce18fb715d249bc9d93fc7d10e817d.json +++ b/.sqlx/query-fce8f4fbd59a8b3b8531e10599914331682d58ace57214bfa26ccaa089592a24.json @@ -1,6 +1,6 @@ { "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,\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 coalesce(message.sent_sequence <= $2, true)\n order by message.sent_sequence\n ", + "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 coalesce(message.sent_sequence <= $2, true)\n order by message.sent_sequence\n ", "describe": { "columns": [ { @@ -19,7 +19,7 @@ "type_info": "Text" }, { - "name": "body", + "name": "body: Body", "ordinal": 3, "type_info": "Text" }, @@ -58,5 +58,5 @@ true ] }, - "hash": "bddc3b0d75f6048c36630db3abb8945a49ce18fb715d249bc9d93fc7d10e817d" + "hash": "fce8f4fbd59a8b3b8531e10599914331682d58ace57214bfa26ccaa089592a24" } @@ -825,6 +825,7 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "unicode-normalization", "uuid", ] @@ -31,6 +31,7 @@ sqlx = { version = "=0.8.2", features = ["chrono", "runtime-tokio", "sqlite"] } thiserror = "1.0.64" tokio = { version = "1.40.0", features = ["rt", "macros", "rt-multi-thread"] } tokio-stream = { version = "0.1.16", features = ["sync"] } +unicode-normalization = "0.1.24" uuid = { version = "1.11.0", features = ["v4"] } [dev-dependencies] diff --git a/docs/api/channels-messages.md b/docs/api/channels-messages.md index 1ff037d..a441f52 100644 --- a/docs/api/channels-messages.md +++ b/docs/api/channels-messages.md @@ -70,6 +70,8 @@ The response will have the following fields: | `id` | string | A unique identifier for the channel. This can be used to associate the channel with events, or to make API calls targeting the channel. | | `name` | string | The channel's name. | +The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name. + When completed, the service will emit a [channel created](events.md#channel-created) event with the channel's ID. ### Duplicate channel name @@ -125,6 +127,8 @@ The response will have the following fields: | `id` | string | A unique identifier for the message. This can be used to associate the message with events, or to make API calls targeting the message. | | `body` | string | The message's body. | +The returned message body may not be identical to the body as sent, as the body will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned body will include this normalization; the service will use the normalized body elsewhere, and does not store the originally submitted body. + When completed, the service will emit a [message sent](events.md#message-sent) event with the message's ID. ### Invalid channel ID diff --git a/docs/api/initial-setup.md b/docs/api/initial-setup.md index 3c5a8a6..306d798 100644 --- a/docs/api/initial-setup.md +++ b/docs/api/initial-setup.md @@ -71,6 +71,10 @@ The response will include the following fields: | `id` | string | A unique identifier for the newly-created login. This can be used to associate the login with other events, or to make API calls targeting the login. | | `name` | string | The login's name. | +The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name. + +The provided password will also be converted to normalization form C. However, the normalized password is not returned to the client. + The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the initial login created for this request. See the [authentication](./authentication) section for details on how this cookie may be used. The cookie will expire if it is not used regularly. diff --git a/docs/api/invitations.md b/docs/api/invitations.md index d3431d7..ddbef8a 100644 --- a/docs/api/invitations.md +++ b/docs/api/invitations.md @@ -150,6 +150,10 @@ The response will include the following fields: | `id` | string | A unique identifier for the newly-created login. This can be used to associate the login with other events, or to make API calls targeting the login. | | `name` | string | The login's name. | +The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name. + +The provided password will also be converted to normalization form C. However, the normalized password is not returned to the client. + The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the login created for this request. See the [authentication](./authentication.md) section for details on how this cookie may be used. The cookie will expire if it is not used regularly. diff --git a/src/channel/app.rs b/src/channel/app.rs index 75c662d..ea60943 100644 --- a/src/channel/app.rs +++ b/src/channel/app.rs @@ -2,7 +2,7 @@ use chrono::TimeDelta; use itertools::Itertools; use sqlx::sqlite::SqlitePool; -use super::{repo::Provider as _, Channel, History, Id}; +use super::{repo::Provider as _, Channel, History, Id, Name}; use crate::{ clock::DateTime, db::{Duplicate as _, NotFound as _}, @@ -20,14 +20,14 @@ impl<'a> Channels<'a> { Self { db, events } } - pub async fn create(&self, name: &str, created_at: &DateTime) -> Result<Channel, CreateError> { + pub async fn create(&self, name: &Name, created_at: &DateTime) -> Result<Channel, CreateError> { let mut tx = self.db.begin().await?; let created = tx.sequence().next(created_at).await?; let channel = tx .channels() .create(name, &created) .await - .duplicate(|| CreateError::DuplicateName(name.into()))?; + .duplicate(|| CreateError::DuplicateName(name.clone()))?; tx.commit().await?; self.events @@ -134,7 +134,7 @@ impl<'a> Channels<'a> { #[derive(Debug, thiserror::Error)] pub enum CreateError { #[error("channel named {0} already exists")] - DuplicateName(String), + DuplicateName(Name), #[error(transparent)] Database(#[from] sqlx::Error), } diff --git a/src/channel/mod.rs b/src/channel/mod.rs index eb8200b..fb13e92 100644 --- a/src/channel/mod.rs +++ b/src/channel/mod.rs @@ -2,8 +2,11 @@ pub mod app; pub mod event; mod history; mod id; +mod name; pub mod repo; mod routes; mod snapshot; -pub use self::{event::Event, history::History, id::Id, routes::router, snapshot::Channel}; +pub use self::{ + event::Event, history::History, id::Id, name::Name, routes::router, snapshot::Channel, +}; diff --git a/src/channel/name.rs b/src/channel/name.rs new file mode 100644 index 0000000..fc82dec --- /dev/null +++ b/src/channel/name.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use crate::nfc; + +#[derive( + Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, sqlx::Type, +)] +#[serde(transparent)] +#[sqlx(transparent)] +pub struct Name(nfc::String); + +impl fmt::Display for Name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self(name) = self; + name.fmt(f) + } +} + +impl From<String> for Name { + fn from(name: String) -> Self { + Self(name.into()) + } +} + +impl From<Name> for String { + fn from(name: Name) -> Self { + let Name(name) = name; + name.into() + } +} diff --git a/src/channel/repo.rs b/src/channel/repo.rs index 27d35f0..3353bfd 100644 --- a/src/channel/repo.rs +++ b/src/channel/repo.rs @@ -1,7 +1,7 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; use crate::{ - channel::{Channel, History, Id}, + channel::{Channel, History, Id, Name}, clock::DateTime, event::{Instant, ResumePoint, Sequence}, }; @@ -19,7 +19,7 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Channels<'t>(&'t mut SqliteConnection); impl<'c> Channels<'c> { - pub async fn create(&mut self, name: &str, created: &Instant) -> Result<History, sqlx::Error> { + pub async fn create(&mut self, name: &Name, created: &Instant) -> Result<History, sqlx::Error> { let id = Id::generate(); let channel = sqlx::query!( r#" @@ -28,7 +28,7 @@ impl<'c> Channels<'c> { values ($1, $2, $3, $4) returning id as "id: Id", - name as "name!", -- known non-null as we just set it + name as "name!: Name", -- known non-null as we just set it created_at as "created_at: DateTime", created_sequence as "created_sequence: Sequence" "#, @@ -57,7 +57,7 @@ impl<'c> Channels<'c> { r#" select id as "id: Id", - channel.name, + channel.name as "name: Name", channel.created_at as "created_at: DateTime", channel.created_sequence as "created_sequence: Sequence", deleted.deleted_at as "deleted_at?: DateTime", @@ -89,7 +89,7 @@ impl<'c> Channels<'c> { r#" select id as "id: Id", - channel.name, + channel.name as "name: Name", channel.created_at as "created_at: DateTime", channel.created_sequence as "created_sequence: Sequence", deleted.deleted_at as "deleted_at: DateTime", @@ -125,7 +125,7 @@ impl<'c> Channels<'c> { r#" select id as "id: Id", - channel.name, + channel.name as "name: Name", channel.created_at as "created_at: DateTime", channel.created_sequence as "created_sequence: Sequence", deleted.deleted_at as "deleted_at: DateTime", @@ -235,7 +235,7 @@ impl<'c> Channels<'c> { r#" select channel.id as "id: Id", - channel.name, + channel.name as "name: Name", channel.created_at as "created_at: DateTime", channel.created_sequence as "created_sequence: Sequence", deleted.deleted_at as "deleted_at?: DateTime", diff --git a/src/channel/routes/channel/post.rs b/src/channel/routes/channel/post.rs index b489a77..d0cae05 100644 --- a/src/channel/routes/channel/post.rs +++ b/src/channel/routes/channel/post.rs @@ -9,7 +9,7 @@ use crate::{ clock::RequestedAt, error::{Internal, NotFound}, login::Login, - message::{app::SendError, Message}, + message::{app::SendError, Body, Message}, }; pub async fn handler( @@ -29,7 +29,7 @@ pub async fn handler( #[derive(serde::Deserialize)] pub struct Request { - pub body: String, + pub body: Body, } #[derive(Debug)] diff --git a/src/channel/routes/post.rs b/src/channel/routes/post.rs index a05c312..d354f79 100644 --- a/src/channel/routes/post.rs +++ b/src/channel/routes/post.rs @@ -6,7 +6,7 @@ use axum::{ use crate::{ app::App, - channel::{app, Channel}, + channel::{app, Channel, Name}, clock::RequestedAt, error::Internal, login::Login, @@ -29,7 +29,7 @@ pub async fn handler( #[derive(serde::Deserialize)] pub struct Request { - pub name: String, + pub name: Name, } #[derive(Debug)] diff --git a/src/channel/snapshot.rs b/src/channel/snapshot.rs index 2b7d89a..dc2894d 100644 --- a/src/channel/snapshot.rs +++ b/src/channel/snapshot.rs @@ -1,13 +1,13 @@ use super::{ event::{Created, Event}, - Id, + Id, Name, }; use crate::clock::DateTime; #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct Channel { pub id: Id, - pub name: String, + pub name: Name, #[serde(skip_serializing_if = "Option::is_none")] pub deleted_at: Option<DateTime>, } diff --git a/src/invite/app.rs b/src/invite/app.rs index ee7f74f..285a819 100644 --- a/src/invite/app.rs +++ b/src/invite/app.rs @@ -6,7 +6,7 @@ use crate::{ clock::DateTime, db::{Duplicate as _, NotFound as _}, event::repo::Provider as _, - login::{repo::Provider as _, Login, Password}, + login::{repo::Provider as _, Login, Name, Password}, token::{repo::Provider as _, Secret}, }; @@ -42,7 +42,7 @@ impl<'a> Invites<'a> { pub async fn accept( &self, invite: &Id, - name: &str, + name: &Name, password: &Password, accepted_at: &DateTime, ) -> Result<(Login, Secret), AcceptError> { @@ -68,7 +68,7 @@ impl<'a> Invites<'a> { .logins() .create(name, &password_hash, &created) .await - .duplicate(|| AcceptError::DuplicateLogin(name.into()))?; + .duplicate(|| AcceptError::DuplicateLogin(name.clone()))?; let secret = tx.tokens().issue(&login, accepted_at).await?; tx.commit().await?; @@ -92,7 +92,7 @@ pub enum AcceptError { #[error("invite not found: {0}")] NotFound(Id), #[error("name in use: {0}")] - DuplicateLogin(String), + DuplicateLogin(Name), #[error(transparent)] Database(#[from] sqlx::Error), #[error(transparent)] diff --git a/src/invite/routes/invite/post.rs b/src/invite/routes/invite/post.rs index c072929..8160465 100644 --- a/src/invite/routes/invite/post.rs +++ b/src/invite/routes/invite/post.rs @@ -9,7 +9,7 @@ use crate::{ clock::RequestedAt, error::{Internal, NotFound}, invite::app, - login::{Login, Password}, + login::{Login, Name, Password}, token::extract::IdentityToken, }; @@ -31,7 +31,7 @@ pub async fn handler( #[derive(serde::Deserialize)] pub struct Request { - pub name: String, + pub name: Name, pub password: Password, } @@ -16,6 +16,7 @@ mod id; mod invite; mod login; mod message; +mod nfc; mod setup; #[cfg(test)] mod test; diff --git a/src/login/app.rs b/src/login/app.rs index b6f7e1c..ebc1c00 100644 --- a/src/login/app.rs +++ b/src/login/app.rs @@ -1,6 +1,6 @@ use sqlx::sqlite::SqlitePool; -use super::{repo::Provider as _, Login, Password}; +use super::{repo::Provider as _, Login, Name, Password}; use crate::{ clock::DateTime, event::{repo::Provider as _, Broadcaster, Event}, @@ -18,7 +18,7 @@ impl<'a> Logins<'a> { pub async fn create( &self, - name: &str, + name: &Name, password: &Password, created_at: &DateTime, ) -> Result<Login, CreateError> { diff --git a/src/login/mod.rs b/src/login/mod.rs index 98cc3d7..71d5bfc 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -4,11 +4,13 @@ pub mod event; pub mod extract; mod history; mod id; +mod name; pub mod password; pub mod repo; mod routes; mod snapshot; pub use self::{ - event::Event, history::History, id::Id, password::Password, routes::router, snapshot::Login, + event::Event, history::History, id::Id, name::Name, password::Password, routes::router, + snapshot::Login, }; diff --git a/src/login/name.rs b/src/login/name.rs new file mode 100644 index 0000000..d882ff9 --- /dev/null +++ b/src/login/name.rs @@ -0,0 +1,28 @@ +use std::fmt; + +use crate::nfc; + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, sqlx::Type)] +#[serde(transparent)] +#[sqlx(transparent)] +pub struct Name(nfc::String); + +impl fmt::Display for Name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self(name) = self; + name.fmt(f) + } +} + +impl From<String> for Name { + fn from(name: String) -> Self { + Self(name.into()) + } +} + +impl From<Name> for String { + fn from(name: Name) -> Self { + let Name(name) = name; + name.into() + } +} diff --git a/src/login/password.rs b/src/login/password.rs index 14fd981..f9ecf37 100644 --- a/src/login/password.rs +++ b/src/login/password.rs @@ -4,6 +4,8 @@ use argon2::Argon2; use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use rand_core::OsRng; +use crate::nfc; + #[derive(sqlx::Type)] #[sqlx(transparent)] pub struct StoredHash(String); @@ -31,7 +33,7 @@ impl fmt::Debug for StoredHash { #[derive(serde::Deserialize)] #[serde(transparent)] -pub struct Password(String); +pub struct Password(nfc::String); impl Password { pub fn hash(&self) -> Result<StoredHash, password_hash::Error> { @@ -56,9 +58,8 @@ impl fmt::Debug for Password { } } -#[cfg(test)] impl From<String> for Password { fn from(password: String) -> Self { - Self(password) + Password(password.into()) } } diff --git a/src/login/repo.rs b/src/login/repo.rs index 7d0fcb1..204329f 100644 --- a/src/login/repo.rs +++ b/src/login/repo.rs @@ -3,7 +3,7 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; use crate::{ clock::DateTime, event::{Instant, ResumePoint, Sequence}, - login::{password::StoredHash, History, Id, Login}, + login::{password::StoredHash, History, Id, Login, Name}, }; pub trait Provider { @@ -21,7 +21,7 @@ pub struct Logins<'t>(&'t mut SqliteConnection); impl<'c> Logins<'c> { pub async fn create( &mut self, - name: &str, + name: &Name, password_hash: &StoredHash, created: &Instant, ) -> Result<History, sqlx::Error> { @@ -34,7 +34,7 @@ impl<'c> Logins<'c> { values ($1, $2, $3, $4, $5) returning id as "id: Id", - name, + name as "name: Name", created_sequence as "created_sequence: Sequence", created_at as "created_at: DateTime" "#, @@ -62,7 +62,7 @@ impl<'c> Logins<'c> { r#" select id as "id: Id", - name, + name as "name: Name", created_sequence as "created_sequence: Sequence", created_at as "created_at: DateTime" from login @@ -88,7 +88,7 @@ impl<'c> Logins<'c> { r#" select id as "id: Id", - name, + name as "name: Name", created_sequence as "created_sequence: Sequence", created_at as "created_at: DateTime" from login diff --git a/src/login/routes/login/post.rs b/src/login/routes/login/post.rs index 67eaa6d..7a685e2 100644 --- a/src/login/routes/login/post.rs +++ b/src/login/routes/login/post.rs @@ -8,7 +8,7 @@ use crate::{ app::App, clock::RequestedAt, error::Internal, - login::{Login, Password}, + login::{Login, Name, Password}, token::{app, extract::IdentityToken}, }; @@ -29,7 +29,7 @@ pub async fn handler( #[derive(serde::Deserialize)] pub struct Request { - pub name: String, + pub name: Name, pub password: Password, } diff --git a/src/login/snapshot.rs b/src/login/snapshot.rs index 1a92f5c..85800e4 100644 --- a/src/login/snapshot.rs +++ b/src/login/snapshot.rs @@ -1,6 +1,6 @@ use super::{ event::{Created, Event}, - Id, + Id, Name, }; // This also implements FromRequestParts (see `./extract.rs`). As a result, it @@ -10,7 +10,7 @@ use super::{ #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct Login { pub id: Id, - pub name: String, + 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. diff --git a/src/message/app.rs b/src/message/app.rs index 4e50513..af87553 100644 --- a/src/message/app.rs +++ b/src/message/app.rs @@ -2,7 +2,7 @@ use chrono::TimeDelta; use itertools::Itertools; use sqlx::sqlite::SqlitePool; -use super::{repo::Provider as _, Id, Message}; +use super::{repo::Provider as _, Body, Id, Message}; use crate::{ channel::{self, repo::Provider as _}, clock::DateTime, @@ -26,7 +26,7 @@ impl<'a> Messages<'a> { channel: &channel::Id, sender: &Login, sent_at: &DateTime, - body: &str, + body: &Body, ) -> Result<Message, SendError> { let mut tx = self.db.begin().await?; let channel = tx diff --git a/src/message/body.rs b/src/message/body.rs new file mode 100644 index 0000000..a415f85 --- /dev/null +++ b/src/message/body.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use crate::nfc; + +#[derive( + Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, sqlx::Type, +)] +#[serde(transparent)] +#[sqlx(transparent)] +pub struct Body(nfc::String); + +impl fmt::Display for Body { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self(body) = self; + body.fmt(f) + } +} + +impl From<String> for Body { + fn from(body: String) -> Self { + Self(body.into()) + } +} + +impl From<Body> for String { + fn from(body: Body) -> Self { + let Body(body) = body; + body.into() + } +} diff --git a/src/message/mod.rs b/src/message/mod.rs index a8f51ab..c2687bc 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -1,4 +1,5 @@ pub mod app; +mod body; pub mod event; mod history; mod id; @@ -6,4 +7,6 @@ pub mod repo; mod routes; mod snapshot; -pub use self::{event::Event, history::History, id::Id, routes::router, snapshot::Message}; +pub use self::{ + body::Body, event::Event, history::History, id::Id, routes::router, snapshot::Message, +}; diff --git a/src/message/repo.rs b/src/message/repo.rs index 85a69fc..4cfefec 100644 --- a/src/message/repo.rs +++ b/src/message/repo.rs @@ -1,6 +1,6 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; -use super::{snapshot::Message, History, Id}; +use super::{snapshot::Message, Body, History, Id}; use crate::{ channel, clock::DateTime, @@ -26,24 +26,24 @@ impl<'c> Messages<'c> { channel: &channel::History, sender: &Login, sent: &Instant, - body: &str, + body: &Body, ) -> Result<History, sqlx::Error> { let id = Id::generate(); let channel_id = channel.id(); let message = sqlx::query!( r#" - insert into message - (id, channel, sender, sent_at, sent_sequence, body) - values ($1, $2, $3, $4, $5, $6) - returning - id as "id: Id", + insert into message + (id, channel, sender, sent_at, sent_sequence, body) + values ($1, $2, $3, $4, $5, $6) + returning + id as "id: Id", channel as "channel: channel::Id", sender as "sender: login::Id", sent_at as "sent_at: DateTime", sent_sequence as "sent_sequence: Sequence", - body - "#, + body as "body: Body" + "#, id, channel_id, sender.id, @@ -76,7 +76,7 @@ impl<'c> Messages<'c> { message.channel as "channel: channel::Id", message.sender as "sender: login::Id", id as "id: Id", - message.body, + message.body as "body: Body", message.sent_at as "sent_at: DateTime", message.sent_sequence as "sent_sequence: Sequence", deleted.deleted_at as "deleted_at: DateTime", @@ -113,7 +113,7 @@ impl<'c> Messages<'c> { message.channel as "channel: channel::Id", message.sender as "sender: login::Id", id as "id: Id", - message.body, + message.body as "body: Body", message.sent_at as "sent_at: DateTime", message.sent_sequence as "sent_sequence: Sequence", deleted.deleted_at as "deleted_at: DateTime", @@ -150,7 +150,7 @@ impl<'c> Messages<'c> { message.channel as "channel: channel::Id", message.sender as "sender: login::Id", id as "id: Id", - message.body, + message.body as "body: Body", message.sent_at as "sent_at: DateTime", message.sent_sequence as "sent_sequence: Sequence", deleted.deleted_at as "deleted_at?: DateTime", @@ -256,7 +256,7 @@ impl<'c> Messages<'c> { message.sender as "sender: login::Id", message.sent_at as "sent_at: DateTime", message.sent_sequence as "sent_sequence: Sequence", - message.body, + message.body as "body: Body", deleted.deleted_at as "deleted_at?: DateTime", deleted.deleted_sequence as "deleted_sequence?: Sequence" from message @@ -293,7 +293,7 @@ impl<'c> Messages<'c> { message.sender as "sender: login::Id", message.sent_at as "sent_at: DateTime", message.sent_sequence as "sent_sequence: Sequence", - message.body, + message.body as "body: Body", deleted.deleted_at as "deleted_at: DateTime", deleted.deleted_sequence as "deleted_sequence: Sequence" from message diff --git a/src/message/snapshot.rs b/src/message/snapshot.rs index 7300918..53b7176 100644 --- a/src/message/snapshot.rs +++ b/src/message/snapshot.rs @@ -1,6 +1,6 @@ use super::{ event::{Event, Sent}, - Id, + Body, Id, }; use crate::{channel, clock::DateTime, event::Instant, login}; @@ -11,7 +11,7 @@ pub struct Message { pub channel: channel::Id, pub sender: login::Id, pub id: Id, - pub body: String, + pub body: Body, #[serde(skip_serializing_if = "Option::is_none")] pub deleted_at: Option<DateTime>, } diff --git a/src/nfc.rs b/src/nfc.rs new file mode 100644 index 0000000..70e936c --- /dev/null +++ b/src/nfc.rs @@ -0,0 +1,103 @@ +use std::{fmt, string::String as StdString}; + +use sqlx::{ + encode::{Encode, IsNull}, + Database, Decode, Type, +}; +use unicode_normalization::UnicodeNormalization as _; + +#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(from = "StdString", into = "StdString")] +pub struct String(StdString); + +impl fmt::Display for String { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self(value) = self; + value.fmt(f) + } +} + +impl From<StdString> for String { + fn from(value: StdString) -> Self { + let value = value.nfc().collect(); + + Self(value) + } +} + +impl From<String> for StdString { + fn from(value: String) -> Self { + let String(value) = value; + value + } +} + +impl std::ops::Deref for String { + type Target = StdString; + + fn deref(&self) -> &Self::Target { + let Self(value) = self; + value + } +} + +// Type is manually implemented so that we can implement Decode to do +// normalization on read. Implementation is otherwise based on +// `#[derive(sqlx::Type)]` with the `#[sqlx(transparent)]` attribute. +impl<DB> Type<DB> for String +where + DB: Database, + StdString: Type<DB>, +{ + fn type_info() -> <DB as Database>::TypeInfo { + <StdString as Type<DB>>::type_info() + } + + fn compatible(ty: &<DB as Database>::TypeInfo) -> bool { + <StdString as Type<DB>>::compatible(ty) + } +} + +impl<'r, DB> Decode<'r, DB> for String +where + DB: Database, + StdString: Decode<'r, DB>, +{ + fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> { + let value = StdString::decode(value)?; + let value = value.nfc().collect(); + Ok(Self(value)) + } +} + +impl<'q, DB> Encode<'q, DB> for String +where + DB: Database, + StdString: Encode<'q, DB>, +{ + fn encode_by_ref( + &self, + buf: &mut <DB as Database>::ArgumentBuffer<'q>, + ) -> Result<IsNull, sqlx::error::BoxDynError> { + let Self(value) = self; + value.encode_by_ref(buf) + } + + fn encode( + self, + buf: &mut <DB as Database>::ArgumentBuffer<'q>, + ) -> Result<IsNull, sqlx::error::BoxDynError> { + let Self(value) = self; + value.encode(buf) + } + + fn produces(&self) -> Option<<DB as Database>::TypeInfo> { + let Self(value) = self; + value.produces() + } + + fn size_hint(&self) -> usize { + let Self(value) = self; + value.size_hint() + } +} diff --git a/src/setup/app.rs b/src/setup/app.rs index d015813..9fbcf6d 100644 --- a/src/setup/app.rs +++ b/src/setup/app.rs @@ -4,7 +4,7 @@ use super::repo::Provider as _; use crate::{ clock::DateTime, event::{repo::Provider as _, Broadcaster, Event}, - login::{repo::Provider as _, Login, Password}, + login::{repo::Provider as _, Login, Name, Password}, token::{repo::Provider as _, Secret}, }; @@ -20,7 +20,7 @@ impl<'a> Setup<'a> { pub async fn initial( &self, - name: &str, + name: &Name, password: &Password, created_at: &DateTime, ) -> Result<(Login, Secret), Error> { diff --git a/src/setup/routes/post.rs b/src/setup/routes/post.rs index 34f4ed2..6a3fa11 100644 --- a/src/setup/routes/post.rs +++ b/src/setup/routes/post.rs @@ -8,7 +8,7 @@ use crate::{ app::App, clock::RequestedAt, error::Internal, - login::{Login, Password}, + login::{Login, Name, Password}, setup::app, token::extract::IdentityToken, }; @@ -30,7 +30,7 @@ pub async fn handler( #[derive(serde::Deserialize)] pub struct Request { - pub name: String, + pub name: Name, pub password: Password, } diff --git a/src/test/fixtures/channel.rs b/src/test/fixtures/channel.rs index a1dda61..024ac1b 100644 --- a/src/test/fixtures/channel.rs +++ b/src/test/fixtures/channel.rs @@ -8,7 +8,7 @@ use rand; use crate::{ app::App, - channel::{self, Channel}, + channel::{self, Channel, Name}, clock::RequestedAt, event::Event, }; @@ -21,13 +21,13 @@ pub async fn create(app: &App, created_at: &RequestedAt) -> Channel { .expect("should always succeed if the channel is actually new") } -pub fn propose() -> String { - rand::random::<Name>().to_string() +pub fn propose() -> Name { + rand::random::<NameTemplate>().to_string().into() } -struct Name(String); +struct NameTemplate(String); faker_impl_from_templates! { - Name; "{} {}", CityName, FullName; + NameTemplate; "{} {}", CityName, FullName; } pub fn events(event: Event) -> future::Ready<Option<channel::Event>> { diff --git a/src/test/fixtures/login.rs b/src/test/fixtures/login.rs index b6766fe..0a42320 100644 --- a/src/test/fixtures/login.rs +++ b/src/test/fixtures/login.rs @@ -4,7 +4,7 @@ use uuid::Uuid; use crate::{ app::App, clock::RequestedAt, - login::{self, Login, Password}, + login::{self, Login, Name, Password}, }; pub async fn create_with_password(app: &App, created_at: &RequestedAt) -> (Login, Password) { @@ -29,16 +29,16 @@ pub async fn create(app: &App, created_at: &RequestedAt) -> Login { pub fn fictitious() -> Login { Login { id: login::Id::generate(), - name: name(), + name: propose_name(), } } -pub fn propose() -> (String, Password) { - (name(), propose_password()) +pub fn propose() -> (Name, Password) { + (propose_name(), propose_password()) } -fn name() -> String { - rand::random::<internet::Username>().to_string() +fn propose_name() -> Name { + rand::random::<internet::Username>().to_string().into() } pub fn propose_password() -> Password { diff --git a/src/test/fixtures/message.rs b/src/test/fixtures/message.rs index eb00e7c..c450bce 100644 --- a/src/test/fixtures/message.rs +++ b/src/test/fixtures/message.rs @@ -8,7 +8,7 @@ use crate::{ clock::RequestedAt, event::Event, login::Login, - message::{self, Message}, + message::{self, Body, Message}, }; pub async fn send(app: &App, channel: &Channel, login: &Login, sent_at: &RequestedAt) -> Message { @@ -20,8 +20,8 @@ pub async fn send(app: &App, channel: &Channel, login: &Login, sent_at: &Request .expect("should succeed if the channel exists") } -pub fn propose() -> String { - rand::random::<Paragraphs>().to_string() +pub fn propose() -> Body { + rand::random::<Paragraphs>().to_string().into() } pub fn events(event: Event) -> future::Ready<Option<message::Event>> { diff --git a/src/token/app.rs b/src/token/app.rs index 0dc1a46..d4dd1a0 100644 --- a/src/token/app.rs +++ b/src/token/app.rs @@ -12,7 +12,7 @@ use super::{ use crate::{ clock::DateTime, db::NotFound as _, - login::{Login, Password}, + login::{Login, Name, Password}, }; pub struct Tokens<'a> { @@ -27,7 +27,7 @@ impl<'a> Tokens<'a> { pub async fn login( &self, - name: &str, + name: &Name, password: &Password, login_at: &DateTime, ) -> Result<(Login, Secret), LoginError> { diff --git a/src/token/repo/auth.rs b/src/token/repo/auth.rs index 88d0878..c621b65 100644 --- a/src/token/repo/auth.rs +++ b/src/token/repo/auth.rs @@ -3,7 +3,7 @@ use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; use crate::{ clock::DateTime, event::{Instant, Sequence}, - login::{self, password::StoredHash, History, Login}, + login::{self, password::StoredHash, History, Login, Name}, }; pub trait Provider { @@ -19,12 +19,12 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Auth<'t>(&'t mut SqliteConnection); impl<'t> Auth<'t> { - pub async fn for_name(&mut self, name: &str) -> Result<(History, StoredHash), sqlx::Error> { + pub async fn for_name(&mut self, name: &Name) -> Result<(History, StoredHash), sqlx::Error> { let found = sqlx::query!( r#" select id as "id: login::Id", - name, + name as "name: Name", password_hash as "password_hash: StoredHash", created_sequence as "created_sequence: Sequence", created_at as "created_at: DateTime" diff --git a/src/token/repo/token.rs b/src/token/repo/token.rs index c592dcd..960bb72 100644 --- a/src/token/repo/token.rs +++ b/src/token/repo/token.rs @@ -3,7 +3,7 @@ use uuid::Uuid; use crate::{ clock::DateTime, - login::{self, History, Login}, + login::{self, History, Login, Name}, token::{Id, Secret}, }; @@ -128,7 +128,7 @@ impl<'c> Tokens<'c> { select token.id as "token_id: Id", login.id as "login_id: login::Id", - login.name as "login_name" + login.name as "login_name: Name" from login join token on login.id = token.login where token.secret = $1 |
