summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--docs/api/channels-messages.md4
-rw-r--r--docs/api/initial-setup.md4
-rw-r--r--docs/api/invitations.md4
-rw-r--r--src/channel/app.rs8
-rw-r--r--src/channel/mod.rs5
-rw-r--r--src/channel/name.rs30
-rw-r--r--src/channel/repo.rs14
-rw-r--r--src/channel/routes/channel/post.rs4
-rw-r--r--src/channel/routes/post.rs4
-rw-r--r--src/channel/snapshot.rs4
-rw-r--r--src/invite/app.rs8
-rw-r--r--src/invite/routes/invite/post.rs4
-rw-r--r--src/lib.rs1
-rw-r--r--src/login/app.rs4
-rw-r--r--src/login/mod.rs4
-rw-r--r--src/login/name.rs28
-rw-r--r--src/login/password.rs7
-rw-r--r--src/login/repo.rs10
-rw-r--r--src/login/routes/login/post.rs4
-rw-r--r--src/login/snapshot.rs4
-rw-r--r--src/message/app.rs4
-rw-r--r--src/message/body.rs30
-rw-r--r--src/message/mod.rs5
-rw-r--r--src/message/repo.rs28
-rw-r--r--src/message/snapshot.rs4
-rw-r--r--src/nfc.rs103
-rw-r--r--src/setup/app.rs4
-rw-r--r--src/setup/routes/post.rs4
-rw-r--r--src/test/fixtures/channel.rs10
-rw-r--r--src/test/fixtures/login.rs12
-rw-r--r--src/test/fixtures/message.rs6
-rw-r--r--src/token/app.rs4
-rw-r--r--src/token/repo/auth.rs6
-rw-r--r--src/token/repo/token.rs4
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"
}
diff --git a/Cargo.lock b/Cargo.lock
index 2c0556a..c4454e3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -825,6 +825,7 @@ dependencies = [
"thiserror",
"tokio",
"tokio-stream",
+ "unicode-normalization",
"uuid",
]
diff --git a/Cargo.toml b/Cargo.toml
index a131601..fe8fdfd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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,
}
diff --git a/src/lib.rs b/src/lib.rs
index 73a2cb0..4d0d9b9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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