diff --git a/.env b/.env new file mode 100644 index 0000000..097ea37 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres://atuin:aeBafah6AiJu@localhost/atuin diff --git a/Cargo.lock b/Cargo.lock index cf70ac8..cb9eb8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,7 +78,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -101,7 +101,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -112,18 +112,23 @@ dependencies = [ "chrono-english", "cli-table", "config", + "diesel", + "diesel_migrations", "directories", + "dotenv", "eyre", + "fern", "hostname", "indicatif", "itertools", "log 0.4.14", - "pretty_env_logger", "rocket", + "rocket_contrib", "rusqlite", "serde 1.0.124", "serde_derive", "shellexpand", + "sodiumoxide", "structopt", "termion", "tui", @@ -252,7 +257,7 @@ dependencies = [ "num-integer", "num-traits 0.2.14", "time", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -331,7 +336,7 @@ dependencies = [ "regex", "terminal_size", "unicode-width", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -409,6 +414,41 @@ dependencies = [ "syn 0.15.44", ] +[[package]] +name = "diesel" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "047bfc4d5c3bd2ef6ca6f981941046113524b9a9f9a7cbdfdd7ff40f58e6f542" +dependencies = [ + "bitflags", + "byteorder", + "chrono", + "diesel_derives", + "pq-sys", + "r2d2", +] + +[[package]] +name = "diesel_derives" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.9", + "syn 1.0.60", +] + +[[package]] +name = "diesel_migrations" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c" +dependencies = [ + "migrations_internals", + "migrations_macros", +] + [[package]] name = "digest" version = "0.8.1" @@ -445,7 +485,7 @@ checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" dependencies = [ "libc", "redox_users 0.3.5", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -456,9 +496,15 @@ checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users 0.4.0", - "winapi", + "winapi 0.3.9", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "either" version = "1.6.1" @@ -471,19 +517,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" -[[package]] -name = "env_logger" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" -dependencies = [ - "atty", - "humantime", - "log 0.4.14", - "regex", - "termcolor", -] - [[package]] name = "eyre" version = "0.6.5" @@ -512,6 +545,62 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9a4820f0ccc8a7afd67c39a0f1a0f4b07ca1725164271a64939d7aeb9af065" +dependencies = [ + "log 0.4.14", +] + +[[package]] +name = "filetime" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.4", + "winapi 0.3.9", +] + +[[package]] +name = "fsevent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" +dependencies = [ + "bitflags", + "fsevent-sys", +] + +[[package]] +name = "fsevent-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" +dependencies = [ + "libc", +] + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + [[package]] name = "generic-array" version = "0.12.3" @@ -622,7 +711,7 @@ checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" dependencies = [ "libc", "match_cfg", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -631,15 +720,6 @@ version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" -[[package]] -name = "humantime" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -dependencies = [ - "quick-error", -] - [[package]] name = "hyper" version = "0.10.16" @@ -698,6 +778,44 @@ dependencies = [ "regex", ] +[[package]] +name = "inotify" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "itertools" version = "0.10.0" @@ -713,6 +831,16 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "language-tags" version = "0.2.2" @@ -725,6 +853,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lexical-core" version = "0.7.5" @@ -744,6 +878,17 @@ version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" +[[package]] +name = "libsodium-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a685b64f837b339074115f2e7f7b431ac73681d08d75b389db7498b8892b8a58" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "libsqlite3-sys" version = "0.20.1" @@ -771,6 +916,15 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +[[package]] +name = "lock_api" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.3.9" @@ -807,6 +961,27 @@ version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "migrations_internals" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860" +dependencies = [ + "diesel", +] + +[[package]] +name = "migrations_macros" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" +dependencies = [ + "migrations_internals", + "proc-macro2 1.0.24", + "quote 1.0.9", + "syn 1.0.60", +] + [[package]] name = "mime" version = "0.2.6" @@ -816,6 +991,60 @@ dependencies = [ "log 0.3.9", ] +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log 0.4.14", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log 0.4.14", + "mio", + "slab", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + [[package]] name = "nom" version = "5.1.2" @@ -827,6 +1056,24 @@ dependencies = [ "version_check 0.9.2", ] +[[package]] +name = "notify" +version = "4.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd" +dependencies = [ + "bitflags", + "filetime", + "fsevent", + "fsevent-sys", + "inotify", + "libc", + "mio", + "mio-extras", + "walkdir", + "winapi 0.3.9", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -889,6 +1136,31 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall 0.2.4", + "smallvec", + "winapi 0.3.9", +] + [[package]] name = "pear" version = "0.1.4" @@ -946,13 +1218,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] -name = "pretty_env_logger" -version = "0.4.0" +name = "pq-sys" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +checksum = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda" dependencies = [ - "env_logger", - "log 0.4.14", + "vcpkg", ] [[package]] @@ -997,12 +1268,6 @@ dependencies = [ "unicode-xid 0.2.1", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quote" version = "0.6.13" @@ -1021,6 +1286,17 @@ dependencies = [ "proc-macro2 1.0.24", ] +[[package]] +name = "r2d2" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" +dependencies = [ + "log 0.4.14", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.7.3" @@ -1161,6 +1437,34 @@ dependencies = [ "yansi", ] +[[package]] +name = "rocket_contrib" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7954a707f9ca18aa74ca8c1f5d1f900f52a4dceb68e96e3112143f759cfd20e" +dependencies = [ + "diesel", + "log 0.4.14", + "notify", + "r2d2", + "rocket", + "rocket_contrib_codegen", + "serde 1.0.124", + "serde_json", +] + +[[package]] +name = "rocket_contrib_codegen" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30deb6dec53b91fac3538a2a3935cf13e0f462745f9f33bf27bedffbe7265b5d" +dependencies = [ + "devise", + "quote 0.6.13", + "version_check 0.9.2", + "yansi", +] + [[package]] name = "rocket_http" version = "0.4.7" @@ -1223,12 +1527,36 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scanlex" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "088c5d71572124929ea7549a8ce98e1a6fd33d0a38367b09027b382e67c033db" +[[package]] +name = "scheduled-thread-pool" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "0.8.23" @@ -1306,12 +1634,29 @@ dependencies = [ "dirs-next", ] +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + [[package]] name = "smallvec" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +[[package]] +name = "sodiumoxide" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7038b67c941e23501573cb7242ffb08709abe9b11eb74bceff875bbda024a6a8" +dependencies = [ + "libc", + "libsodium-sys", + "serde 1.0.124", +] + [[package]] name = "state" version = "0.4.2" @@ -1404,7 +1749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1444,7 +1789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1616,6 +1961,17 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +[[package]] +name = "walkdir" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -1628,6 +1984,12 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -1638,6 +2000,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -1650,7 +2018,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1659,6 +2027,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 783c020..b5f4a57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ description = "atuin - magical shell history" [dependencies] log = "0.4" -pretty_env_logger = "0.4" +fern = "0.6.0" chrono = "0.4" eyre = "0.6" shellexpand = "2" @@ -27,7 +27,16 @@ tui = "0.14" termion = "1.5" unicode-width = "0.1" itertools = "0.10.0" +diesel = { version = "1.4.4", features = ["postgres", "chrono"] } +diesel_migrations = "1.4.0" +dotenv = "0.15.0" +sodiumoxide = "0.2.6" [dependencies.rusqlite] version = "0.24" features = ["bundled"] + +[dependencies.rocket_contrib] +version = "0.4.7" +default-features = false +features = ["diesel_postgres_pool", "json"] diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..92267c8 --- /dev/null +++ b/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" diff --git a/migrations/.gitkeep b/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/2021-03-20-151809_create_history/down.sql b/migrations/2021-03-20-151809_create_history/down.sql new file mode 100644 index 0000000..ea02ce4 --- /dev/null +++ b/migrations/2021-03-20-151809_create_history/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +drop table history; diff --git a/migrations/2021-03-20-151809_create_history/up.sql b/migrations/2021-03-20-151809_create_history/up.sql new file mode 100644 index 0000000..7cb19fc --- /dev/null +++ b/migrations/2021-03-20-151809_create_history/up.sql @@ -0,0 +1,11 @@ +-- Your SQL goes here +-- lower case SQL please, this isn't a shouting match +create table history ( + id bigserial primary key, + client_id text not null unique, -- the client-generated ID + user_id bigserial not null, -- allow multiple users + mac varchar(128) not null, -- store a hashed mac address, to identify machines - more likely to be unique than hostname + timestamp timestamp not null, -- one of the few non-encrypted metadatas + + data varchar(8192) not null -- store the actual history data, encrypted. I don't wanna know! +); diff --git a/migrations/2021-03-20-171007_create_users/down.sql b/migrations/2021-03-20-171007_create_users/down.sql new file mode 100644 index 0000000..5795f6b --- /dev/null +++ b/migrations/2021-03-20-171007_create_users/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +drop table users; diff --git a/migrations/2021-03-20-171007_create_users/up.sql b/migrations/2021-03-20-171007_create_users/up.sql new file mode 100644 index 0000000..0eecea7 --- /dev/null +++ b/migrations/2021-03-20-171007_create_users/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +create table users ( + id bigserial primary key, -- also store our own ID + email varchar(128) not null unique, -- being able to contact users is useful + password varchar(128) not null unique +); diff --git a/migrations/2021-03-21-181750_create_sessions/down.sql b/migrations/2021-03-21-181750_create_sessions/down.sql new file mode 100644 index 0000000..53a779c --- /dev/null +++ b/migrations/2021-03-21-181750_create_sessions/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +drop table sessions; diff --git a/migrations/2021-03-21-181750_create_sessions/up.sql b/migrations/2021-03-21-181750_create_sessions/up.sql new file mode 100644 index 0000000..b81705e --- /dev/null +++ b/migrations/2021-03-21-181750_create_sessions/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +create table sessions ( + id bigserial primary key, + user_id bigserial, + token varchar(128) unique not null +); diff --git a/src/command/mod.rs b/src/command/mod.rs index 3ebb92e..a5ea022 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -49,7 +49,7 @@ impl AtuinCmd { match self { Self::History(history) => history.run(db), Self::Import(import) => import.run(db), - Self::Server(server) => server.run(), + Self::Server(server) => server.run(settings), Self::Stats(stats) => stats.run(db, settings), Self::Init => init::init(), Self::Search { query } => search::run(&query, db), diff --git a/src/command/server.rs b/src/command/server.rs index 1ddc73e..9d9bcb3 100644 --- a/src/command/server.rs +++ b/src/command/server.rs @@ -2,6 +2,7 @@ use eyre::Result; use structopt::StructOpt; use crate::remote::server; +use crate::settings::Settings; #[derive(StructOpt)] pub enum Cmd { @@ -10,8 +11,8 @@ pub enum Cmd { #[allow(clippy::unused_self)] // I'll use it later impl Cmd { - pub fn run(&self) -> Result<()> { - server::launch(); + pub fn run(&self, settings: &Settings) -> Result<()> { + server::launch(settings); Ok(()) } } diff --git a/src/main.rs b/src/main.rs index d47866f..3c4a05e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,15 @@ extern crate rocket; #[macro_use] extern crate serde_derive; +#[macro_use] +extern crate diesel; + +#[macro_use] +extern crate diesel_migrations; + +#[macro_use] +extern crate rocket_contrib; + use command::AtuinCmd; use local::database::Sqlite; use settings::Settings; @@ -26,6 +35,8 @@ mod local; mod remote; mod settings; +pub mod schema; + #[derive(StructOpt)] #[structopt( author = "Ellie Huxtable ", @@ -61,7 +72,18 @@ impl Atuin { } fn main() -> Result<()> { - pretty_env_logger::init(); + fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} [{}] {}", + chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), + record.level(), + message + )) + }) + .level(log::LevelFilter::Info) + .chain(std::io::stdout()) + .apply()?; Atuin::from_args().run() } diff --git a/src/remote/auth.rs b/src/remote/auth.rs new file mode 100644 index 0000000..8f9e9b4 --- /dev/null +++ b/src/remote/auth.rs @@ -0,0 +1,200 @@ +use self::diesel::prelude::*; +use rocket::http::Status; +use rocket::request::{self, FromRequest, Outcome, Request}; +use rocket_contrib::databases::diesel; +use sodiumoxide::crypto::pwhash::argon2id13; + +use rocket_contrib::json::Json; +use uuid::Uuid; + +use super::models::{NewSession, NewUser, Session, User}; +use super::views::ApiResponse; +use crate::schema::{sessions, users}; + +use super::database::AtuinDbConn; + +#[derive(Debug)] +pub enum KeyError { + Missing, + Invalid, +} + +pub fn hash_str(secret: &str) -> String { + sodiumoxide::init().unwrap(); + let hash = argon2id13::pwhash( + secret.as_bytes(), + argon2id13::OPSLIMIT_INTERACTIVE, + argon2id13::MEMLIMIT_INTERACTIVE, + ) + .unwrap(); + let texthash = std::str::from_utf8(&hash.0).unwrap().to_string(); + + // postgres hates null chars. don't do that to postgres + texthash.trim_end_matches('\u{0}').to_string() +} + +pub fn verify_str(secret: &str, verify: &str) -> bool { + sodiumoxide::init().unwrap(); + + let mut padded = [0_u8; 128]; + secret.as_bytes().iter().enumerate().for_each(|(i, val)| { + padded[i] = *val; + }); + + match argon2id13::HashedPassword::from_slice(&padded) { + Some(hp) => argon2id13::pwhash_verify(&hp, verify.as_bytes()), + None => false, + } +} + +impl<'a, 'r> FromRequest<'a, 'r> for User { + type Error = KeyError; + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let session: Vec<_> = request.headers().get("authorization").collect(); + + if session.is_empty() { + return Outcome::Failure((Status::BadRequest, KeyError::Missing)); + } else if session.len() > 1 { + return Outcome::Failure((Status::BadRequest, KeyError::Invalid)); + } + + let session: Vec<_> = session[0].split(' ').collect(); + + if session.len() != 2 { + return Outcome::Failure((Status::BadRequest, KeyError::Invalid)); + } + + if session[0] != "Token" { + return Outcome::Failure((Status::BadRequest, KeyError::Invalid)); + } + + let session = session[1]; + + let db = request + .guard::() + .succeeded() + .expect("failed to load database"); + + let session = sessions::table + .filter(sessions::token.eq(session)) + .first::(&*db); + + if session.is_err() { + return Outcome::Failure((Status::Unauthorized, KeyError::Invalid)); + } + + let session = session.unwrap(); + + let user = users::table.find(session.user_id).first(&*db); + + match user { + Ok(user) => Outcome::Success(user), + Err(_) => Outcome::Failure((Status::Unauthorized, KeyError::Invalid)), + } + } +} + +#[derive(Deserialize)] +pub struct Register { + email: String, + password: String, +} + +#[post("/register", data = "")] +#[allow(clippy::clippy::needless_pass_by_value)] +pub fn register(conn: AtuinDbConn, register: Json) -> ApiResponse { + let hashed = hash_str(register.password.as_str()); + + let new_user = NewUser { + email: register.email.as_str(), + password: hashed.as_str(), + }; + + let user = diesel::insert_into(users::table) + .values(&new_user) + .get_result(&*conn); + + if user.is_err() { + return ApiResponse { + status: Status::BadRequest, + json: json!({ + "status": "error", + "message": "failed to create user - is the email already in use?", + }), + }; + } + + let user: User = user.unwrap(); + let token = Uuid::new_v4().to_simple().to_string(); + + let new_session = NewSession { + user_id: user.id, + token: token.as_str(), + }; + + match diesel::insert_into(sessions::table) + .values(&new_session) + .execute(&*conn) + { + Ok(_) => ApiResponse { + status: Status::Ok, + json: json!({"status": "ok", "message": "user created!", "session": token}), + }, + Err(_) => ApiResponse { + status: Status::BadRequest, + json: json!({"status": "error", "message": "failed to create user"}), + }, + } +} + +#[derive(Deserialize)] +pub struct Login { + email: String, + password: String, +} + +#[post("/login", data = "")] +#[allow(clippy::clippy::needless_pass_by_value)] +pub fn login(conn: AtuinDbConn, login: Json) -> ApiResponse { + let user = users::table + .filter(users::email.eq(login.email.as_str())) + .first(&*conn); + + if user.is_err() { + return ApiResponse { + status: Status::NotFound, + json: json!({"status": "error", "message": "user not found"}), + }; + } + + let user: User = user.unwrap(); + + let session = sessions::table + .filter(sessions::user_id.eq(user.id)) + .first(&*conn); + + // a session should exist... + if session.is_err() { + return ApiResponse { + status: Status::InternalServerError, + json: json!({"status": "error", "message": "something went wrong"}), + }; + } + + let verified = verify_str(user.password.as_str(), login.password.as_str()); + + if !verified { + return ApiResponse { + status: Status::NotFound, + json: json!({"status": "error", "message": "user not found"}), + }; + } + + let session: Session = session.unwrap(); + + ApiResponse { + status: Status::Ok, + json: json!({"status": "ok", "token": session.token}), + } +} diff --git a/src/remote/database.rs b/src/remote/database.rs new file mode 100644 index 0000000..4f386de --- /dev/null +++ b/src/remote/database.rs @@ -0,0 +1,14 @@ +use diesel::pg::PgConnection; +use diesel::prelude::*; + +use crate::settings::Settings; + +#[database("atuin")] +pub struct AtuinDbConn(diesel::PgConnection); + +// TODO: connection pooling +pub fn establish_connection(settings: &Settings) -> PgConnection { + let database_url = &settings.remote.db.url; + PgConnection::establish(database_url) + .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) +} diff --git a/src/remote/mod.rs b/src/remote/mod.rs index 74f47ad..7147b88 100644 --- a/src/remote/mod.rs +++ b/src/remote/mod.rs @@ -1 +1,5 @@ +pub mod auth; +pub mod database; +pub mod models; pub mod server; +pub mod views; diff --git a/src/remote/models.rs b/src/remote/models.rs new file mode 100644 index 0000000..058b2f0 --- /dev/null +++ b/src/remote/models.rs @@ -0,0 +1,56 @@ +use chrono::naive::NaiveDateTime; + +use crate::schema::{history, sessions, users}; + +#[derive(Identifiable, Queryable, Associations)] +#[table_name = "history"] +#[belongs_to(User)] +pub struct History { + pub id: i64, + pub client_id: String, + pub user_id: i64, + pub mac: String, + pub timestamp: NaiveDateTime, + + pub data: String, +} + +#[derive(Identifiable, Queryable, Associations)] +pub struct User { + pub id: i64, + pub email: String, + pub password: String, +} + +#[derive(Queryable, Identifiable, Associations)] +#[belongs_to(User)] +pub struct Session { + pub id: i64, + pub user_id: i64, + pub token: String, +} + +#[derive(Insertable)] +#[table_name = "history"] +pub struct NewHistory<'a> { + pub client_id: &'a str, + pub user_id: i64, + pub mac: &'a str, + pub timestamp: NaiveDateTime, + + pub data: &'a str, +} + +#[derive(Insertable)] +#[table_name = "users"] +pub struct NewUser<'a> { + pub email: &'a str, + pub password: &'a str, +} + +#[derive(Insertable)] +#[table_name = "sessions"] +pub struct NewSession<'a> { + pub user_id: i64, + pub token: &'a str, +} diff --git a/src/remote/server.rs b/src/remote/server.rs index bc1dc2b..4409f64 100644 --- a/src/remote/server.rs +++ b/src/remote/server.rs @@ -1,8 +1,42 @@ -#[get("/")] -const fn index() -> &'static str { - "Hello, world!" -} +use rocket::config::{Config, Environment, LoggingLevel, Value}; -pub fn launch() { - rocket::ignite().mount("/", routes![index]).launch(); +use std::collections::HashMap; + +use crate::remote::database::establish_connection; +use crate::settings::Settings; + +use super::database::AtuinDbConn; + +// a bunch of these imports are generated by macros, it's easier to wildcard +#[allow(clippy::clippy::wildcard_imports)] +use super::views::*; + +#[allow(clippy::clippy::wildcard_imports)] +use super::auth::*; + +embed_migrations!("migrations"); + +pub fn launch(settings: &Settings) { + let mut database_config = HashMap::new(); + let mut databases = HashMap::new(); + + database_config.insert("url", Value::from(settings.remote.db.url.clone())); + databases.insert("atuin", Value::from(database_config)); + + let connection = establish_connection(settings); + embedded_migrations::run(&connection).expect("failed to run migrations"); + + let config = Config::build(Environment::Production) + .address("0.0.0.0") + .log_level(LoggingLevel::Normal) + .port(8080) + .extra("databases", databases) + .finalize() + .unwrap(); + + let app = rocket::custom(config); + app.mount("/", routes![index, register, add_history, login]) + .attach(AtuinDbConn::fairing()) + .register(catchers![internal_error, bad_request]) + .launch(); } diff --git a/src/remote/views.rs b/src/remote/views.rs new file mode 100644 index 0000000..2af3f36 --- /dev/null +++ b/src/remote/views.rs @@ -0,0 +1,89 @@ +use self::diesel::prelude::*; +use rocket::http::{ContentType, Status}; +use rocket::request::Request; +use rocket::response; +use rocket::response::{Responder, Response}; +use rocket_contrib::databases::diesel; +use rocket_contrib::json::{Json, JsonValue}; + +use super::database::AtuinDbConn; +use super::models::{NewHistory, User}; +use crate::schema::history; + +#[derive(Debug)] +pub struct ApiResponse { + pub json: JsonValue, + pub status: Status, +} + +impl<'r> Responder<'r> for ApiResponse { + fn respond_to(self, req: &Request) -> response::Result<'r> { + Response::build_from(self.json.respond_to(req).unwrap()) + .status(self.status) + .header(ContentType::JSON) + .ok() + } +} + +#[get("/")] +pub const fn index() -> &'static str { + "\"Through the fathomless deeps of space swims the star turtle Great A\u{2019}Tuin, bearing on its back the four giant elephants who carry on their shoulders the mass of the Discworld.\"\n\t-- Sir Terry Pratchett" +} + +#[catch(500)] +pub fn internal_error(_req: &Request) -> ApiResponse { + ApiResponse { + status: Status::InternalServerError, + json: json!({"status": "error", "message": "an internal server error has occured"}), + } +} + +#[catch(400)] +pub fn bad_request(_req: &Request) -> ApiResponse { + ApiResponse { + status: Status::InternalServerError, + json: json!({"status": "error", "message": "bad request. don't do that."}), + } +} + +#[derive(Deserialize)] +pub struct AddHistory { + id: String, + timestamp: i64, + data: String, + mac: String, +} + +#[post("/history", data = "")] +#[allow( + clippy::clippy::cast_sign_loss, + clippy::cast_possible_truncation, + clippy::clippy::needless_pass_by_value +)] +pub fn add_history(conn: AtuinDbConn, user: User, add_history: Json) -> ApiResponse { + let secs: i64 = add_history.timestamp / 1_000_000_000; + let nanosecs: u32 = (add_history.timestamp - (secs * 1_000_000_000)) as u32; + let datetime = chrono::NaiveDateTime::from_timestamp(secs, nanosecs); + + let new_history = NewHistory { + client_id: add_history.id.as_str(), + user_id: user.id, + mac: add_history.mac.as_str(), + timestamp: datetime, + data: add_history.data.as_str(), + }; + + match diesel::insert_into(history::table) + .values(&new_history) + .execute(&*conn) + { + Ok(_) => ApiResponse { + status: Status::Ok, + json: json!({"status": "ok", "message": "history added", "id": new_history.client_id}), + }, + Err(_) => ApiResponse { + status: Status::BadRequest, + json: json!({"status": "error", "message": "failed to add history"}), + }, + } +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..efa9ddc --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,28 @@ +table! { + history (id) { + id -> Int8, + client_id -> Text, + user_id -> Int8, + mac -> Varchar, + timestamp -> Timestamp, + data -> Varchar, + } +} + +table! { + sessions (id) { + id -> Int8, + user_id -> Int8, + token -> Varchar, + } +} + +table! { + users (id) { + id -> Int8, + email -> Varchar, + password -> Varchar, + } +} + +allow_tables_to_appear_in_same_query!(history, sessions, users,); diff --git a/src/settings.rs b/src/settings.rs index a4c9f8d..6f29afd 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -10,6 +10,11 @@ pub struct LocalDatabase { pub path: String, } +#[derive(Debug, Deserialize)] +pub struct RemoteDatabase { + pub url: String, +} + #[derive(Debug, Deserialize)] pub struct Local { pub server_address: String, @@ -17,9 +22,15 @@ pub struct Local { pub db: LocalDatabase, } +#[derive(Debug, Deserialize)] +pub struct Remote { + pub db: RemoteDatabase, +} + #[derive(Debug, Deserialize)] pub struct Settings { pub local: Local, + pub remote: Remote, } impl Settings { @@ -49,6 +60,8 @@ impl Settings { s.set_default("local.dialect", "us")?; s.set_default("local.db.path", db_path.to_str())?; + s.set_default("remote.db.url", "please set a postgres url")?; + if config_file.exists() { s.merge(File::with_name(config_file.to_str().unwrap()))?; }