Implement server (#23)

* Add initial database and server setup

* Set up all routes, auth, etc

* Implement sessions, password auth, hashing with argon2, and history storage
This commit is contained in:
Ellie Huxtable 2021-03-21 20:04:39 +00:00 committed by GitHub
parent 716c7722cd
commit c9579cb9ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 980 additions and 55 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=postgres://atuin:aeBafah6AiJu@localhost/atuin

466
Cargo.lock generated
View File

@ -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"

View File

@ -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"]

5
diesel.toml Normal file
View File

@ -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"

0
migrations/.gitkeep Normal file
View File

View File

@ -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();

View File

@ -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;

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
drop table history;

View File

@ -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!
);

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
drop table users;

View File

@ -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
);

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
drop table sessions;

View File

@ -0,0 +1,6 @@
-- Your SQL goes here
create table sessions (
id bigserial primary key,
user_id bigserial,
token varchar(128) unique not null
);

View File

@ -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),

View File

@ -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(())
}
}

View File

@ -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 <e@elm.sh>",
@ -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()
}

200
src/remote/auth.rs Normal file
View File

@ -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<User, Self::Error> {
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::<AtuinDbConn>()
.succeeded()
.expect("failed to load database");
let session = sessions::table
.filter(sessions::token.eq(session))
.first::<Session>(&*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 = "<register>")]
#[allow(clippy::clippy::needless_pass_by_value)]
pub fn register(conn: AtuinDbConn, register: Json<Register>) -> 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 = "<login>")]
#[allow(clippy::clippy::needless_pass_by_value)]
pub fn login(conn: AtuinDbConn, login: Json<Login>) -> 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}),
}
}

14
src/remote/database.rs Normal file
View File

@ -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))
}

View File

@ -1 +1,5 @@
pub mod auth;
pub mod database;
pub mod models;
pub mod server;
pub mod views;

56
src/remote/models.rs Normal file
View File

@ -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,
}

View File

@ -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();
}

89
src/remote/views.rs Normal file
View File

@ -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 = "<add_history>")]
#[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<AddHistory>) -> 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"}),
},
}
}

28
src/schema.rs Normal file
View File

@ -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,);

View File

@ -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()))?;
}