forked from Chewing_Bever/rusty-bever
Compare commits
73 Commits
Author | SHA1 | Date |
---|---|---|
|
e2003442e2 | |
|
19a21b8cdf | |
|
ce97f36c18 | |
|
a107d9e283 | |
|
d013bd60bd | |
|
449c20fac2 | |
|
924feb662d | |
|
3e9c5e4fe7 | |
|
5cbb1c1a97 | |
|
8e433c3250 | |
|
061a9d9bc6 | |
|
18f717685a | |
|
f2a0401601 | |
|
769d7a32de | |
|
f6e9039b59 | |
|
0da8eb127c | |
|
cae6632cf6 | |
|
1441e3e601 | |
|
548dd0d022 | |
|
6d83c18036 | |
|
3e7612a9a8 | |
|
8534090f0f | |
|
211e31a008 | |
|
3d024db2e9 | |
|
a295237863 | |
|
a6b1b0ff76 | |
|
505907d3a1 | |
|
f50008ff99 | |
|
fb2a6126fe | |
|
2cc4d53961 | |
|
3cf7661faf | |
|
02011e04ce | |
|
1378219fe5 | |
|
87d8d8ff0c | |
|
c64eaf2ff5 | |
|
27b904b3f5 | |
|
6858e9da62 | |
|
dd51d107e3 | |
|
b61b329996 | |
|
7d11d4ad17 | |
|
de8be87036 | |
|
055d1f9e8d | |
|
85cfe6290c | |
|
c912c0aa0b | |
|
a100ea52a0 | |
|
1ee9b78d81 | |
|
a8cd8618a3 | |
|
456c947ecd | |
|
159da81b8d | |
|
b4fc6fe2c0 | |
|
7afdd02712 | |
|
16ddc9aecd | |
|
b13b760e2f | |
|
b45c93cdc9 | |
|
d7333373bb | |
|
7dffbb9597 | |
|
badf68e579 | |
|
cc7a668ab0 | |
|
89851a2018 | |
|
dab90bc4a9 | |
|
35fb38de9e | |
|
210eeee7b6 | |
|
13259249fd | |
|
0d4d96d761 | |
|
ac762a3c31 | |
|
9309ec77fb | |
|
7a97b99bd6 | |
|
6782fecc0d | |
|
d90dbcdc2a | |
|
1c524f181f | |
|
5e86133651 | |
|
eefaf7acaa | |
|
4ccee64323 |
|
@ -0,0 +1,3 @@
|
||||||
|
# vim: ft=toml
|
||||||
|
[build]
|
||||||
|
target-dir = "out/target"
|
|
@ -0,0 +1,14 @@
|
||||||
|
*
|
||||||
|
|
||||||
|
!.cargo/
|
||||||
|
!Cargo.lock
|
||||||
|
!Cargo.toml
|
||||||
|
!Makefile
|
||||||
|
!migrations
|
||||||
|
!Rb.yaml
|
||||||
|
!rustfmt.toml
|
||||||
|
!src
|
||||||
|
!tests
|
||||||
|
!web
|
||||||
|
|
||||||
|
web/node_modules
|
|
@ -0,0 +1,7 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = false
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
|
@ -2,7 +2,7 @@
|
||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
debug/
|
debug/
|
||||||
target/
|
out/
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
@ -16,3 +16,6 @@ target/
|
||||||
# Added by cargo
|
# Added by cargo
|
||||||
|
|
||||||
/target
|
/target
|
||||||
|
|
||||||
|
.vim/
|
||||||
|
vendor/
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# API Design
|
||||||
|
|
||||||
|
This file describes the API that the software adheres to. All routes are defined under a shared `api` namespace.
|
||||||
|
|
||||||
|
`(A)` means the route can only be accessed by an admin user.
|
||||||
|
|
||||||
|
## v1
|
||||||
|
|
||||||
|
## Authentification
|
||||||
|
|
||||||
|
* POST `/auth/login` - generate new JWT & refresh token pair given user credentials
|
||||||
|
* POST `/auth/refresh` - generate new JWT & refresh token pair given valid refresh token
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
* GET `/posts?<offset>&<limit>` - get list of posts from the default feed given offset & limit
|
||||||
|
* GET `/posts?<section_id_or_shortname>&<offset>&<limit>` - get list of posts of a specific section
|
||||||
|
* (A) POST `/posts` - create a new post
|
||||||
|
* GET `/posts/<id>` - get a specific post
|
||||||
|
* (A) DELETE `/posts/<id>` - delete a post
|
||||||
|
* (A) PATCH `/posts/<id>` - patch a post
|
||||||
|
|
||||||
|
## Sections
|
||||||
|
|
||||||
|
* GET `/sections?<offset>&<limit>` - get list of sections
|
||||||
|
* GET `/sections/<id_or_shortname>` - get specific section
|
||||||
|
* (A) POST `/sections` - create a new section
|
||||||
|
* (A) PATCH `/sections/<id_or_shortname>` - patch a section
|
||||||
|
* (A) DELETE `/sections/<id_or_shortname>` - delete a section (what happens with posts?)
|
||||||
|
|
||||||
|
## Users
|
||||||
|
|
||||||
|
* (A) GET `/users?<offset>&<limit>`
|
||||||
|
* (A) POST `/users`
|
||||||
|
* (A) GET `/users/<id_or_username>`
|
||||||
|
* (A) PATCH `/users/<id_or_username>`
|
||||||
|
* (A) DELETE `/users/<id_or_username>`
|
||||||
|
|
||||||
|
## Feeds
|
||||||
|
|
||||||
|
WIP
|
|
@ -2,6 +2,18 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayref"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-stream"
|
name = "async-stream"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -66,6 +78,12 @@ version = "0.2.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
|
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "binascii"
|
name = "binascii"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
@ -78,6 +96,26 @@ version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2b_simd"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
|
||||||
|
dependencies = [
|
||||||
|
"arrayref",
|
||||||
|
"arrayvec",
|
||||||
|
"constant_time_eq",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.7.0"
|
version = "3.7.0"
|
||||||
|
@ -108,12 +146,32 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"time 0.1.44",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const_fn"
|
name = "const_fn"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
|
checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.15.1"
|
version = "0.15.1"
|
||||||
|
@ -121,10 +179,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d"
|
checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"time",
|
"time 0.2.27",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-mac"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "devise"
|
name = "devise"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -166,9 +253,11 @@ checksum = "bba51ca66f57261fd17cadf8b73e4775cc307d0521d855de3f5de91a8f074e0e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
"chrono",
|
||||||
"diesel_derives",
|
"diesel_derives",
|
||||||
"pq-sys",
|
"pq-sys",
|
||||||
"r2d2",
|
"r2d2",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -192,12 +281,27 @@ dependencies = [
|
||||||
"migrations_macros",
|
"migrations_macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "discard"
|
name = "discard"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
|
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dtoa"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
@ -222,6 +326,7 @@ dependencies = [
|
||||||
"atomic",
|
"atomic",
|
||||||
"pear",
|
"pear",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_yaml",
|
||||||
"toml",
|
"toml",
|
||||||
"uncased",
|
"uncased",
|
||||||
"version_check",
|
"version_check",
|
||||||
|
@ -355,6 +460,16 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
@ -406,6 +521,16 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-mac",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
@ -496,6 +621,21 @@ version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jwt"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7487a802642fa9b162acacad3ed52c7e47ed4108d5fac6125cc7742dfaf622bf"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"crypto-mac",
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -508,6 +648,21 @@ version = "0.2.99"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
|
checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libmimalloc-sys"
|
||||||
|
version = "0.1.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d1b8479c593dba88c2741fc50b92e13dbabbbe0bd504d979f244ccc1a5b1c01"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linked-hash-map"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
|
@ -566,6 +721,15 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mimalloc"
|
||||||
|
version = "0.1.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb74897ce508e6c49156fd1476fc5922cbc6e75183c65e399c765a09122e5130"
|
||||||
|
dependencies = [
|
||||||
|
"libmimalloc-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.16"
|
version = "0.3.16"
|
||||||
|
@ -623,6 +787,25 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
|
@ -639,6 +822,12 @@ version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
|
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opaque-debug"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.36"
|
version = "0.10.36"
|
||||||
|
@ -912,13 +1101,15 @@ dependencies = [
|
||||||
"rocket_codegen",
|
"rocket_codegen",
|
||||||
"rocket_http",
|
"rocket_http",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"state",
|
"state",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"time",
|
"time 0.2.27",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"ubyte",
|
"ubyte",
|
||||||
|
"uuid",
|
||||||
"version_check",
|
"version_check",
|
||||||
"yansi",
|
"yansi",
|
||||||
]
|
]
|
||||||
|
@ -962,9 +1153,10 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"stable-pattern",
|
"stable-pattern",
|
||||||
"state",
|
"state",
|
||||||
"time",
|
"time 0.2.27",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uncased",
|
"uncased",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -991,6 +1183,18 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-argon2"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"blake2b_simd",
|
||||||
|
"constant_time_eq",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
@ -1010,11 +1214,22 @@ checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
|
||||||
name = "rusty-bever"
|
name = "rusty-bever"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"chrono",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
|
"figment",
|
||||||
|
"hmac",
|
||||||
|
"jwt",
|
||||||
|
"mimalloc",
|
||||||
"openssl",
|
"openssl",
|
||||||
|
"rand",
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_sync_db_pools",
|
"rocket_sync_db_pools",
|
||||||
|
"rust-argon2",
|
||||||
|
"serde",
|
||||||
|
"sha2",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1090,12 +1305,37 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.8.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "039ba818c784248423789eec090aab9fb566c7b94d6ebbfa1814a9fd52c8afb2"
|
||||||
|
dependencies = [
|
||||||
|
"dtoa",
|
||||||
|
"linked-hash-map",
|
||||||
|
"serde",
|
||||||
|
"yaml-rust",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
|
checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
"opaque-debug",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -1209,6 +1449,12 @@ version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
|
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.74"
|
version = "1.0.74"
|
||||||
|
@ -1234,6 +1480,17 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.1.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.2.27"
|
version = "0.2.27"
|
||||||
|
@ -1378,6 +1635,12 @@ dependencies = [
|
||||||
"unchecked-index",
|
"unchecked-index",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ubyte"
|
name = "ubyte"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
@ -1409,6 +1672,15 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
@ -1433,9 +1705,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.10.2+wasi-snapshot-preview1"
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
|
@ -1513,6 +1785,15 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaml-rust"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||||
|
dependencies = [
|
||||||
|
"linked-hash-map",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yansi"
|
name = "yansi"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
47
Cargo.toml
47
Cargo.toml
|
@ -4,23 +4,46 @@ version = "0.1.0"
|
||||||
authors = ["Jef Roosens <roosensjef@gmail.com>"]
|
authors = ["Jef Roosens <roosensjef@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "rb"
|
|
||||||
path = "src/rb/lib.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "rbs"
|
name = "rbd"
|
||||||
path = "src/rbs/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
web = []
|
||||||
|
docs = []
|
||||||
|
static = ["web", "docs"]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = "0.5.0-rc.1"
|
# Backend web framework
|
||||||
diesel = { version = "1.4.7", features = ["postgres"] }
|
rocket = { version = "0.5.0-rc.1", features = [ "json", "uuid" ] }
|
||||||
|
# Used to provide Rocket routes with database connections
|
||||||
|
rocket_sync_db_pools = { version = "0.1.0-rc.1", default_features = false, features = [ "diesel_postgres_pool" ] }
|
||||||
|
# Used to (de)serialize JSON
|
||||||
|
serde = { version = "1.0.127", features = [ "derive" ] }
|
||||||
|
# ORM
|
||||||
|
diesel = { version = "1.4.7", features = ["postgres", "uuidv07", "chrono"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "1.4.0"
|
||||||
|
# To properly compile libpq statically
|
||||||
openssl = "0.10.36"
|
openssl = "0.10.36"
|
||||||
|
# For password hashing & verification
|
||||||
|
rust-argon2 = "0.8.3"
|
||||||
|
rand = "0.8.4"
|
||||||
|
uuid = { version = "0.8.2", features = ["serde"] }
|
||||||
|
# Authentification
|
||||||
|
jwt = "0.14.0"
|
||||||
|
hmac = "*"
|
||||||
|
sha2 = "*"
|
||||||
|
# Timestamps for JWT tokens
|
||||||
|
chrono = { version = "*", features = [ "serde" ] }
|
||||||
|
# Encoding of refresh tokens
|
||||||
|
base64 = "0.13.0"
|
||||||
|
# Reading in configuration files
|
||||||
|
figment = { version = "*", features = [ "yaml" ] }
|
||||||
|
mimalloc = { version = "0.1.26", default_features = false }
|
||||||
|
|
||||||
[dependencies.rocket_sync_db_pools]
|
[profile.release]
|
||||||
version = "0.1.0-rc.1"
|
lto = "fat"
|
||||||
default_features = false
|
panic = "abort"
|
||||||
features = ["diesel_postgres_pool"]
|
codegen-units = 1
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Build frontend files
|
||||||
|
FROM node:16 AS fbuilder
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY web/ ./
|
||||||
|
|
||||||
|
RUN yarn install && \
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
|
||||||
|
# Build backend & backend docs
|
||||||
|
FROM rust:1.55-alpine AS builder
|
||||||
|
|
||||||
|
ARG DI_VER=1.2.5
|
||||||
|
|
||||||
|
# ENV OPENSSL_STATIC=1 \
|
||||||
|
# PQ_LIB_STATIC=1
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk add --no-cache \
|
||||||
|
postgresql \
|
||||||
|
postgresql-dev \
|
||||||
|
openssl-dev \
|
||||||
|
build-base
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Build backend
|
||||||
|
COPY .cargo/ ./.cargo
|
||||||
|
COPY src/ ./src
|
||||||
|
COPY migrations/ ./migrations
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
|
RUN cargo build --release && \
|
||||||
|
cargo doc --no-deps
|
||||||
|
|
||||||
|
# Build dumb-init
|
||||||
|
RUN curl -sSL "https://github.com/Yelp/dumb-init/archive/refs/tags/v$DI_VER.tar.gz" | \
|
||||||
|
tar -xzf - && \
|
||||||
|
cd "dumb-init-$DI_VER" && \
|
||||||
|
make build && \
|
||||||
|
mv dumb-init ..
|
||||||
|
|
||||||
|
|
||||||
|
FROM alpine:3.14.2
|
||||||
|
|
||||||
|
RUN mkdir -p /var/www/html
|
||||||
|
|
||||||
|
COPY --from=fbuilder /usr/src/app/dist /var/www/html/site
|
||||||
|
COPY --from=builder /usr/src/app/out/target/doc /var/www/html/doc
|
||||||
|
COPY --from=builder /usr/src/app/out/target/release/rbd /usr/bin/rbd
|
||||||
|
COPY --from=builder /usr/src/app/dumb-init /usr/bin/dumb-init
|
||||||
|
|
||||||
|
ENTRYPOINT [ "dumb-init", "--" ]
|
||||||
|
CMD [ "/usr/bin/rbd" ]
|
||||||
|
|
||||||
|
# RUN apt update && \
|
||||||
|
# apt install -y --no-install-recommends \
|
||||||
|
# musl-dev \
|
||||||
|
# musl-tools \
|
||||||
|
# libpq-dev \
|
||||||
|
# libssl-dev && \
|
||||||
|
# rustup target add x86_64-unknown-linux-musl && \
|
||||||
|
# mkdir "$PREFIX" && \
|
||||||
|
# echo "$PREFIX/lib" >> /etc/ld-musl-x86_64.path
|
|
@ -0,0 +1,140 @@
|
||||||
|
# =====CONFIGURATION=====
|
||||||
|
# Version of postgresql to compile libpq from
|
||||||
|
PQ_VER ?= 11.12
|
||||||
|
# OpenSSL version
|
||||||
|
SSL_VER ?= 1.1.1k
|
||||||
|
# Dumb-init version
|
||||||
|
DI_VER ?= 1.2.5
|
||||||
|
|
||||||
|
|
||||||
|
# =====AUTO-GENERATED VARIABLES=====
|
||||||
|
# This is such a lovely oneliner
|
||||||
|
# NOTE: $(dir PATH) outputs a trailing slash
|
||||||
|
OUT_DIR ?= $(dir $(abspath $(lastword $(MAKEFILE_LIST))))out
|
||||||
|
|
||||||
|
PREFIX := $(OUT_DIR)/prefix
|
||||||
|
OPENSSL_DIR := $(OUT_DIR)/openssl-$(SSL_VER)
|
||||||
|
PQ_DIR := $(OUT_DIR)/postgresql-$(PQ_VER)
|
||||||
|
DI_DIR := $(OUT_DIR)/dumb-init-$(DI_VER)
|
||||||
|
|
||||||
|
# Used in various make calls to specify parallel recipes
|
||||||
|
CORES != nproc
|
||||||
|
|
||||||
|
|
||||||
|
# =====ENVIRONMENT VARIABLES=====
|
||||||
|
export CC := musl-gcc -fPIC -pie -static
|
||||||
|
export LD_LIBRARY_PATH := $(PREFIX)
|
||||||
|
export PKG_CONFIG_PATH := /usr/local/lib/pkgconfig
|
||||||
|
export PATH := /usr/local/bin:/root/.cargo/bin:$(PATH)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO check for header files (openssl-dev, libpq-dev) both for Arch & Ubuntu
|
||||||
|
|
||||||
|
# Create the out dir
|
||||||
|
$(shell mkdir -p "$(PREFIX)")
|
||||||
|
|
||||||
|
|
||||||
|
# =====BUILDING THE STATIC BINARY=====
|
||||||
|
.PHONY: all
|
||||||
|
all: build
|
||||||
|
|
||||||
|
.PHONY: builder
|
||||||
|
builder:
|
||||||
|
docker build \
|
||||||
|
-t rusty-builder:latest - < docker/Dockerfile.builder
|
||||||
|
|
||||||
|
.PHONY: docker
|
||||||
|
docker: builder
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v "$$PWD:/usr/src" \
|
||||||
|
--workdir "/usr/src" \
|
||||||
|
-it \
|
||||||
|
rusty-builder:latest \
|
||||||
|
bash build.sh
|
||||||
|
|
||||||
|
|
||||||
|
# libpq builds openssl as a dependency
|
||||||
|
.PHONY: build
|
||||||
|
build: libpq
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean: clean-openssl clean-libpq clean-di
|
||||||
|
@ echo "Note: this only cleans the C dependencies, not the Cargo cache."
|
||||||
|
rm -rf "$(PREFIX)"
|
||||||
|
|
||||||
|
# This is used inside the Dockerfile
|
||||||
|
.PHONY: pathfile
|
||||||
|
pathfile:
|
||||||
|
echo "$(PREFIX)/lib" >> /etc/ld-musl-x86_64.path
|
||||||
|
|
||||||
|
|
||||||
|
## =====OPENSSL=====
|
||||||
|
# Download the source code & configure the project
|
||||||
|
$(OPENSSL_DIR)/Configure:
|
||||||
|
curl -sSL "https://www.openssl.org/source/openssl-$(SSL_VER).tar.gz" | \
|
||||||
|
tar -xzC "$(OUT_DIR)"
|
||||||
|
cd "$(OPENSSL_DIR)" && \
|
||||||
|
CC="$(CC) -idirafter /usr/include -idirafter /usr/include/x86_64-linux-gnu/" ./Configure \
|
||||||
|
no-zlib \
|
||||||
|
no-shared \
|
||||||
|
--prefix="$(PREFIX)" \
|
||||||
|
--openssldir="$(PREFIX)/ssl" \
|
||||||
|
linux-x86_64
|
||||||
|
|
||||||
|
# Build OpenSSL
|
||||||
|
.PHONY: openssl
|
||||||
|
openssl: $(OPENSSL_DIR)/Configure
|
||||||
|
cd "$(OPENSSL_DIR)" && env C_INCLUDE_PATH="$(PREFIX)/include" $(MAKE) depend 2> /dev/null
|
||||||
|
cd "$(OPENSSL_DIR)" && $(MAKE) -j$(CORES)
|
||||||
|
cd "$(OPENSSL_DIR)" && $(MAKE) install_sw
|
||||||
|
|
||||||
|
.PHONY: clean-openssl
|
||||||
|
clean-openssl:
|
||||||
|
rm -rf "$(OPENSSL_DIR)"
|
||||||
|
|
||||||
|
|
||||||
|
## =====LIBPQ=====
|
||||||
|
# Download the source code & configure the project
|
||||||
|
$(PQ_DIR)/configure:
|
||||||
|
curl -sSL "https://ftp.postgresql.org/pub/source/v$(PQ_VER)/postgresql-$(PQ_VER).tar.gz" | \
|
||||||
|
tar -xzC "$(OUT_DIR)"
|
||||||
|
cd "$(PQ_DIR)" && \
|
||||||
|
LDFLAGS="-L$(PREFIX)/lib" CFLAGS="-I$(PREFIX)/include" ./configure \
|
||||||
|
--without-readline \
|
||||||
|
--with-openssl \
|
||||||
|
--without-zlib \
|
||||||
|
--prefix="$(PREFIX)" \
|
||||||
|
--host=x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
.PHONY: libpq
|
||||||
|
libpq: openssl $(PQ_DIR)/configure
|
||||||
|
cd "$(PQ_DIR)/src/interfaces/libpq" && $(MAKE) -j$(CORES) all-static-lib
|
||||||
|
cd "$(PQ_DIR)/src/interfaces/libpq" && $(MAKE) install install-lib-static
|
||||||
|
cd "$(PQ_DIR)/src/bin/pg_config" && $(MAKE) -j$(CORES)
|
||||||
|
cd "$(PQ_DIR)/src/bin/pg_config" && $(MAKE) install
|
||||||
|
|
||||||
|
.PHONY: clean-libpq
|
||||||
|
clean-libpq:
|
||||||
|
rm -rf "$(PQ_DIR)"
|
||||||
|
|
||||||
|
|
||||||
|
# =====DUMB-INIT=====
|
||||||
|
# NOTE: this is only used inside the Docker image, but it's here for completeness.
|
||||||
|
$(DI_DIR)/Makefile:
|
||||||
|
curl -sSL "https://github.com/Yelp/dumb-init/archive/refs/tags/v$(DI_VER).tar.gz" | \
|
||||||
|
tar -C "$(OUT_DIR)" -xz
|
||||||
|
|
||||||
|
.PHONY: di
|
||||||
|
di: $(DI_DIR)/Makefile
|
||||||
|
make -C "$(DI_DIR)" build
|
||||||
|
|
||||||
|
.PHONY: clean-di
|
||||||
|
clean-di:
|
||||||
|
rm -rf "$(DI_DIR)"
|
||||||
|
|
||||||
|
|
||||||
|
# ====UTILITIES FOR DEVELOPMENT=====
|
||||||
|
## The tests require a database, so we run them like this
|
||||||
|
test:
|
||||||
|
docker-compose -f docker-compose.test.yml -p rb_test up
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Roadmap
|
||||||
|
|
||||||
|
This file describes a general plan for the software, divided into versions.
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
* Version 1 of backend API
|
||||||
|
* Read-only frontend (no login)
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
Version 0.1.0 will be the first deployable version. The goal is to replace my
|
||||||
|
current blog with an instance of v0.1.0. This includes developing a (basic) SDK
|
||||||
|
(probably in Python) that allows me to interact with my instance, or rather
|
||||||
|
just post stuff.
|
||||||
|
|
||||||
|
## v1.0.0
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
* First stable release
|
||||||
|
* Base for all other releases
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
For me, a 1.0 release indicates that the project is stable and can be actively
|
||||||
|
and efficiently worked on. I basically just want to iron out any wrinkles from
|
||||||
|
the 0.1 release, so that I have a solid base to develop all other features on.
|
||||||
|
This will also allow me to better combine the development of this project with
|
||||||
|
my studies, as it can be properly planned and managed whenever I have the time.
|
||||||
|
Any other features won't appear in this file. Rather, they will be managed
|
||||||
|
using the milestones & issues on my Gitea instance.
|
|
@ -0,0 +1,43 @@
|
||||||
|
default:
|
||||||
|
address: "0.0.0.0"
|
||||||
|
ports: 8000
|
||||||
|
|
||||||
|
debug:
|
||||||
|
keep_alive: 5
|
||||||
|
read_timeout: 5
|
||||||
|
write_timeout: 5
|
||||||
|
log_level: "normal"
|
||||||
|
limits:
|
||||||
|
forms: 32768
|
||||||
|
|
||||||
|
admin_user: "admin"
|
||||||
|
admin_pass: "password"
|
||||||
|
jwt:
|
||||||
|
key: "secret"
|
||||||
|
refresh_token_size: 64
|
||||||
|
# Just 5 seconds for debugging
|
||||||
|
refresh_token_expire: 60
|
||||||
|
|
||||||
|
databases:
|
||||||
|
postgres_rb:
|
||||||
|
url: "postgres://rb:rb@localhost:5432/rb"
|
||||||
|
|
||||||
|
release:
|
||||||
|
keep_alive: 5
|
||||||
|
read_timeout: 5
|
||||||
|
write_timeout: 5
|
||||||
|
log_level: "normal"
|
||||||
|
limits:
|
||||||
|
forms: 32768
|
||||||
|
|
||||||
|
admin_user: "admin"
|
||||||
|
admin_pass: "password"
|
||||||
|
jwt:
|
||||||
|
key: "secret"
|
||||||
|
refresh_token_size: 64
|
||||||
|
# Just 5 seconds for debugging
|
||||||
|
refresh_token_expire: 60
|
||||||
|
|
||||||
|
databases:
|
||||||
|
postgres_rb:
|
||||||
|
url: "postgres://rb:rb@localhost:5432/rb"
|
13
Rocket.toml
13
Rocket.toml
|
@ -1,13 +0,0 @@
|
||||||
[debug]
|
|
||||||
port = 8000
|
|
||||||
keep_alive = 5
|
|
||||||
read_timeout = 5
|
|
||||||
write_timeout = 5
|
|
||||||
log_level = "normal"
|
|
||||||
limits = { forms = 32768 }
|
|
||||||
|
|
||||||
[debug.databases]
|
|
||||||
postgres_rb = { url = "postgres://rb:rb@localhost:5432/rb" }
|
|
||||||
|
|
||||||
[release.databases]
|
|
||||||
postgres_rb = { url = "postgres://rb:rb@db:5432/rb" }
|
|
|
@ -2,4 +2,4 @@
|
||||||
# see diesel.rs/guides/configuring-diesel-cli
|
# see diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
[print_schema]
|
[print_schema]
|
||||||
file = "src/rb/schema.rs"
|
file = "src/schema.rs"
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: '.'
|
||||||
|
dockerfile: 'docker/test/Dockerfile'
|
||||||
|
|
||||||
|
image: 'rb-builder:1.54'
|
||||||
|
command: "${CMD}"
|
||||||
|
|
||||||
|
working_dir: "/usr/src/app"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- '$PWD:/usr/src/app'
|
||||||
|
- 'cache:/usr/src/app/out'
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: 'postgres:13-alpine'
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- 'POSTGRES_DB=rb'
|
||||||
|
- 'POSTGRES_USER=rb'
|
||||||
|
- 'POSTGRES_PASSWORD=rb'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
cache:
|
|
@ -0,0 +1,28 @@
|
||||||
|
# vim: ft=dockerfile
|
||||||
|
FROM rust:1.54
|
||||||
|
|
||||||
|
ENV PREFIX="/usr/src/out/prefix" \
|
||||||
|
CC="musl-gcc -fPIC -pie -static" \
|
||||||
|
LD_LIBRARY_PATH="$PREFIX" \
|
||||||
|
PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" \
|
||||||
|
PATH="/usr/local/bin:/root/.cargo/bin:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
RUN groupadd -g 1000 builder && \
|
||||||
|
useradd -u 1000 -g 1000 builder && \
|
||||||
|
mkdir -p "$PREFIX" && \
|
||||||
|
chown -R builder:builder /usr/src/app && \
|
||||||
|
apt update && \
|
||||||
|
apt install -y --no-install-recommends \
|
||||||
|
musl-dev \
|
||||||
|
musl-tools \
|
||||||
|
libpq-dev \
|
||||||
|
libssl-dev && \
|
||||||
|
rustup target add x86_64-unknown-linux-musl && \
|
||||||
|
echo "$PREFIX/lib" >> /etc/ld-musl-x86_64.path
|
||||||
|
|
||||||
|
|
||||||
|
USER builder
|
||||||
|
|
||||||
|
CMD ["cargo", "test"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE IF EXISTS users, refresh_tokens CASCADE;
|
|
@ -0,0 +1,23 @@
|
||||||
|
CREATE TABLE users (
|
||||||
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
|
||||||
|
username varchar(32) UNIQUE NOT NULL,
|
||||||
|
-- Hashed + salted representation of the username
|
||||||
|
password text NOT NULL,
|
||||||
|
-- Wether the user is currently blocked
|
||||||
|
blocked boolean NOT NULL DEFAULT false,
|
||||||
|
-- Wether the user is an admin
|
||||||
|
admin boolean NOT NULL DEFAULT false
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Stores refresh tokens
|
||||||
|
CREATE TABLE refresh_tokens (
|
||||||
|
-- This is more efficient than storing the text
|
||||||
|
token bytea PRIMARY KEY,
|
||||||
|
-- The user for whom the token was created
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
-- When the token expires
|
||||||
|
expires_at timestamp NOT NULL,
|
||||||
|
-- When the token was last used (is NULL until used)
|
||||||
|
last_used_at timestamp
|
||||||
|
);
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
drop trigger insert_enforce_post_titles on posts;
|
||||||
|
drop trigger update_enforce_post_titles on posts;
|
||||||
|
drop function enforce_post_titles;
|
||||||
|
|
||||||
|
drop table posts cascade;
|
||||||
|
drop table sections cascade;
|
|
@ -0,0 +1,58 @@
|
||||||
|
-- Your SQL goes here
|
||||||
|
create table sections (
|
||||||
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
|
||||||
|
-- Title of the section
|
||||||
|
title varchar(255) UNIQUE NOT NULL,
|
||||||
|
-- Name to use when routing (this just makes for prettier URLs)
|
||||||
|
shortname varchar(32) UNIQUE NOT NULL,
|
||||||
|
-- Optional description of the section
|
||||||
|
description text,
|
||||||
|
-- Wether to show the section in the default list on the homepage
|
||||||
|
is_default boolean NOT NULL DEFAULT false,
|
||||||
|
-- Wether the posts should contain titles or not
|
||||||
|
has_titles boolean NOT NULL DEFAULT true
|
||||||
|
);
|
||||||
|
|
||||||
|
create table posts (
|
||||||
|
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||||
|
|
||||||
|
section_id uuid NOT NULL REFERENCES sections(id) ON DELETE CASCADE,
|
||||||
|
-- Title of the post
|
||||||
|
-- Wether this is NULL or not is enforced using the enforce_post_titles trigger
|
||||||
|
title varchar(255),
|
||||||
|
-- Post date, defaults to today
|
||||||
|
publish_date date NOT NULL DEFAULT now(),
|
||||||
|
-- Content of the post
|
||||||
|
content text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
create function enforce_post_titles() returns trigger as $enforce_post_titles$
|
||||||
|
begin
|
||||||
|
-- Check for a wrongfully null title
|
||||||
|
if new.title is null and exists (
|
||||||
|
select 1 from sections where id = new.section_id and has_titles
|
||||||
|
) then
|
||||||
|
raise exception 'Expected a post title, but got null.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if new.title is not null and exists (
|
||||||
|
select 1 from sections where id = new.section_id and not has_titles
|
||||||
|
) then
|
||||||
|
raise exception 'Expected an empty post title, but got a value.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$enforce_post_titles$ language plpgsql;
|
||||||
|
|
||||||
|
create trigger insert_enforce_post_titles
|
||||||
|
before insert on posts
|
||||||
|
for each row
|
||||||
|
execute function enforce_post_titles();
|
||||||
|
|
||||||
|
create trigger update_enforce_post_titles
|
||||||
|
before update of title on posts
|
||||||
|
for each row
|
||||||
|
when (old.title is distinct from new.title)
|
||||||
|
execute function enforce_post_titles();
|
|
@ -0,0 +1,69 @@
|
||||||
|
binop_separator = "Front"
|
||||||
|
blank_lines_lower_bound = 0
|
||||||
|
blank_lines_upper_bound = 1
|
||||||
|
# Trying something new
|
||||||
|
brace_style = "AlwaysNextLine"
|
||||||
|
color = "Auto"
|
||||||
|
combine_control_expr = false
|
||||||
|
comment_width = 80
|
||||||
|
condense_wildcard_suffixes = false
|
||||||
|
control_brace_style = "AlwaysSameLine"
|
||||||
|
disable_all_formatting = false
|
||||||
|
edition = "2018"
|
||||||
|
emit_mode = "Files"
|
||||||
|
empty_item_single_line = true
|
||||||
|
enum_discrim_align_threshold = 0
|
||||||
|
error_on_line_overflow = false
|
||||||
|
error_on_unformatted = false
|
||||||
|
fn_args_layout = "Tall"
|
||||||
|
fn_single_line = false
|
||||||
|
force_explicit_abi = true
|
||||||
|
force_multiline_blocks = false
|
||||||
|
format_code_in_doc_comments = false
|
||||||
|
format_macro_bodies = true
|
||||||
|
format_macro_matchers = false
|
||||||
|
format_strings = false
|
||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
hard_tabs = false
|
||||||
|
hide_parse_errors = false
|
||||||
|
ignore = []
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
imports_indent = "Block"
|
||||||
|
imports_layout = "Mixed"
|
||||||
|
indent_style = "Block"
|
||||||
|
inline_attribute_width = 0
|
||||||
|
license_template_path = ""
|
||||||
|
make_backup = false
|
||||||
|
match_arm_blocks = true
|
||||||
|
match_arm_leading_pipes = "Never"
|
||||||
|
match_block_trailing_comma = true
|
||||||
|
max_width = 100
|
||||||
|
merge_derives = true
|
||||||
|
newline_style = "Auto"
|
||||||
|
normalize_comments = false
|
||||||
|
normalize_doc_attributes = false
|
||||||
|
overflow_delimited_expr = false
|
||||||
|
remove_nested_parens = true
|
||||||
|
reorder_impl_items = false
|
||||||
|
reorder_imports = true
|
||||||
|
reorder_modules = true
|
||||||
|
report_fixme = "Always"
|
||||||
|
report_todo = "Always"
|
||||||
|
required_version = "1.4.37"
|
||||||
|
skip_children = false
|
||||||
|
space_after_colon = true
|
||||||
|
space_before_colon = false
|
||||||
|
spaces_around_ranges = false
|
||||||
|
struct_field_align_threshold = 0
|
||||||
|
struct_lit_single_line = true
|
||||||
|
tab_spaces = 4
|
||||||
|
trailing_comma = "Vertical"
|
||||||
|
trailing_semicolon = true
|
||||||
|
type_punctuation_density = "Wide"
|
||||||
|
unstable_features = false
|
||||||
|
use_field_init_shorthand = false
|
||||||
|
use_small_heuristics = "Default"
|
||||||
|
use_try_shorthand = false
|
||||||
|
version = "One"
|
||||||
|
where_single_line = false
|
||||||
|
wrap_comments = false
|
|
@ -0,0 +1,58 @@
|
||||||
|
use diesel::PgConnection;
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth::pass::hash_password,
|
||||||
|
db,
|
||||||
|
errors::{RbError, RbResult},
|
||||||
|
guards::Admin,
|
||||||
|
RbDbConn,
|
||||||
|
};
|
||||||
|
|
||||||
|
// #[get("/users")]
|
||||||
|
// pub async fn get_users(_admin: Admin, conn: RbDbConn) -> RbResult<Json<Vec<db::User>>>
|
||||||
|
// {
|
||||||
|
// Ok(Json(conn.run(|c| db::users::all(c)).await?))
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[post("/users", data = "<user>")]
|
||||||
|
pub async fn create_user(_admin: Admin, conn: RbDbConn, user: Json<db::NewUser>) -> RbResult<()>
|
||||||
|
{
|
||||||
|
Ok(conn
|
||||||
|
.run(move |c| db::users::create(c, &user.into_inner()))
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/users/<user_id_str>")]
|
||||||
|
pub async fn get_user_info(
|
||||||
|
_admin: Admin,
|
||||||
|
conn: RbDbConn,
|
||||||
|
user_id_str: &str,
|
||||||
|
) -> RbResult<Json<db::User>>
|
||||||
|
{
|
||||||
|
let user_id = Uuid::parse_str(user_id_str).map_err(|_| RbError::UMUnknownUser)?;
|
||||||
|
|
||||||
|
match conn.run(move |c| db::users::find(c, user_id)).await {
|
||||||
|
Some(user) => Ok(Json(user)),
|
||||||
|
None => Err(RbError::UMUnknownUser),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) -> RbResult<bool>
|
||||||
|
{
|
||||||
|
let pass_hashed = hash_password(password)?;
|
||||||
|
let new_user = db::NewUser {
|
||||||
|
username: username.to_string(),
|
||||||
|
password: pass_hashed,
|
||||||
|
admin: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if db::users::find_by_username(conn, username).is_ok() {
|
||||||
|
db::users::create(conn, &new_user);
|
||||||
|
}
|
||||||
|
// db::users::create_or_update(conn, &new_user)
|
||||||
|
// .map_err(|_| RbError::Custom("Couldn't create admin."))?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
use chrono::Utc;
|
||||||
|
use diesel::PgConnection;
|
||||||
|
use hmac::{Hmac, NewMac};
|
||||||
|
use jwt::SignWithKey;
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db,
|
||||||
|
errors::{RbError, RbResult},
|
||||||
|
RbJwtConf,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct JWTResponse
|
||||||
|
{
|
||||||
|
token: String,
|
||||||
|
refresh_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Claims
|
||||||
|
{
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub username: String,
|
||||||
|
pub admin: bool,
|
||||||
|
pub exp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_jwt_token(
|
||||||
|
conn: &PgConnection,
|
||||||
|
jwt: &RbJwtConf,
|
||||||
|
user: &db::User,
|
||||||
|
) -> RbResult<JWTResponse>
|
||||||
|
{
|
||||||
|
let key: Hmac<Sha256> = Hmac::new_from_slice(jwt.key.as_bytes())
|
||||||
|
.map_err(|_| RbError::Custom("Couldn't create Hmac key."))?;
|
||||||
|
|
||||||
|
let current_time = Utc::now();
|
||||||
|
|
||||||
|
// Create the claims
|
||||||
|
let claims = Claims {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username.clone(),
|
||||||
|
admin: user.admin,
|
||||||
|
exp: current_time.timestamp() + jwt.refresh_token_expire,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign the claims into a new token
|
||||||
|
let token = claims
|
||||||
|
.sign_with_key(&key)
|
||||||
|
.map_err(|_| RbError::Custom("Couldn't sign JWT."))?;
|
||||||
|
|
||||||
|
// Generate a random refresh token
|
||||||
|
let mut refresh_token = vec![0u8; jwt.refresh_token_size];
|
||||||
|
thread_rng().fill(&mut refresh_token[..]);
|
||||||
|
|
||||||
|
let refresh_expire =
|
||||||
|
(current_time + chrono::Duration::seconds(jwt.refresh_token_expire)).naive_utc();
|
||||||
|
|
||||||
|
// Store refresh token in database
|
||||||
|
db::tokens::create(
|
||||||
|
conn,
|
||||||
|
&db::NewRefreshToken {
|
||||||
|
token: refresh_token.to_vec(),
|
||||||
|
user_id: user.id,
|
||||||
|
expires_at: refresh_expire,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(JWTResponse {
|
||||||
|
token,
|
||||||
|
refresh_token: base64::encode(refresh_token),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_token(
|
||||||
|
conn: &PgConnection,
|
||||||
|
jwt: &RbJwtConf,
|
||||||
|
refresh_token: &str,
|
||||||
|
) -> RbResult<JWTResponse>
|
||||||
|
{
|
||||||
|
let token_bytes =
|
||||||
|
base64::decode(refresh_token).map_err(|_| RbError::AuthInvalidRefreshToken)?;
|
||||||
|
|
||||||
|
// First, we request the token from the database to see if it's really a valid token
|
||||||
|
let (token_entry, user) =
|
||||||
|
db::tokens::find_with_user(conn, &token_bytes).ok_or(RbError::AuthInvalidRefreshToken)?;
|
||||||
|
|
||||||
|
// If we see that the token has already been used before, we block the user.
|
||||||
|
if token_entry.last_used_at.is_some() {
|
||||||
|
// If we fail to block the user, the end user must know
|
||||||
|
if let Err(err) = db::users::block(conn, token_entry.user_id) {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(RbError::AuthDuplicateRefreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then we check if the user is blocked
|
||||||
|
if user.blocked {
|
||||||
|
return Err(RbError::AuthBlockedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we check if the token has already expired
|
||||||
|
let cur_time = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
if token_entry.expires_at < cur_time {
|
||||||
|
return Err(RbError::AuthTokenExpired);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We update the last_used_at value for the refresh token
|
||||||
|
db::tokens::update_last_used_at(conn, &token_entry.token, cur_time)?;
|
||||||
|
|
||||||
|
generate_jwt_token(conn, jwt, &user)
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
use rocket::{serde::json::Json, State};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
jwt::{generate_jwt_token, JWTResponse},
|
||||||
|
pass::verify_user,
|
||||||
|
};
|
||||||
|
use crate::{errors::RbResult, guards::User, RbConfig, RbDbConn};
|
||||||
|
|
||||||
|
pub mod jwt;
|
||||||
|
pub mod pass;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Credentials
|
||||||
|
{
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/login")]
|
||||||
|
pub async fn already_logged_in(_user: User) -> String
|
||||||
|
{
|
||||||
|
String::from("You're already logged in!")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/login", data = "<credentials>", rank = 2)]
|
||||||
|
pub async fn login(
|
||||||
|
conn: RbDbConn,
|
||||||
|
conf: &State<RbConfig>,
|
||||||
|
credentials: Json<Credentials>,
|
||||||
|
) -> RbResult<Json<JWTResponse>>
|
||||||
|
{
|
||||||
|
let credentials = credentials.into_inner();
|
||||||
|
let jwt = conf.jwt.clone();
|
||||||
|
|
||||||
|
// Get the user, if credentials are valid
|
||||||
|
let user = conn
|
||||||
|
.run(move |c| verify_user(c, &credentials.username, &credentials.password))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(
|
||||||
|
conn.run(move |c| generate_jwt_token(c, &jwt, &user))
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RefreshTokenRequest
|
||||||
|
{
|
||||||
|
pub refresh_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/refresh", data = "<refresh_token_request>")]
|
||||||
|
pub async fn refresh_token(
|
||||||
|
conn: RbDbConn,
|
||||||
|
conf: &State<RbConfig>,
|
||||||
|
refresh_token_request: Json<RefreshTokenRequest>,
|
||||||
|
) -> RbResult<Json<JWTResponse>>
|
||||||
|
{
|
||||||
|
let refresh_token = refresh_token_request.into_inner().refresh_token;
|
||||||
|
let jwt = conf.jwt.clone();
|
||||||
|
|
||||||
|
Ok(Json(
|
||||||
|
conn.run(move |c| crate::auth::jwt::refresh_token(c, &jwt, &refresh_token))
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
use argon2::verify_encoded;
|
||||||
|
use diesel::PgConnection;
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db,
|
||||||
|
errors::{RbError, RbResult},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> RbResult<db::User>
|
||||||
|
{
|
||||||
|
// TODO handle non-"NotFound" Diesel errors accordingely
|
||||||
|
let user = db::users::find_by_username(conn, username).map_err(|_| RbError::AuthUnknownUser)?;
|
||||||
|
|
||||||
|
// Check if a user is blocked
|
||||||
|
if user.blocked {
|
||||||
|
return Err(RbError::AuthBlockedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
match verify_encoded(user.password.as_str(), password.as_bytes()) {
|
||||||
|
Ok(true) => Ok(user),
|
||||||
|
_ => Err(RbError::AuthInvalidPassword),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash_password(password: &str) -> RbResult<String>
|
||||||
|
{
|
||||||
|
// Generate a random salt
|
||||||
|
let mut salt = [0u8; 64];
|
||||||
|
thread_rng().fill(&mut salt[..]);
|
||||||
|
|
||||||
|
// Encode the actual password
|
||||||
|
let config = argon2::Config::default();
|
||||||
|
argon2::hash_encoded(password.as_bytes(), &salt, &config)
|
||||||
|
.map_err(|_| RbError::Custom("Couldn't hash password."))
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//! The db module contains all Diesel-related logic. This is to prevent the various Diesel imports
|
||||||
|
//! from poluting other modules' namespaces.
|
||||||
|
|
||||||
|
pub mod posts;
|
||||||
|
pub mod sections;
|
||||||
|
pub mod tokens;
|
||||||
|
pub mod users;
|
||||||
|
|
||||||
|
pub use posts::{NewPost, PatchPost, Post};
|
||||||
|
pub use sections::{NewSection, Section};
|
||||||
|
pub use tokens::{NewRefreshToken, RefreshToken};
|
||||||
|
pub use users::{NewUser, User};
|
|
@ -0,0 +1,85 @@
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{RbError, RbOption, RbResult},
|
||||||
|
schema::{posts, posts::dsl::*},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Queryable, Serialize)]
|
||||||
|
pub struct Post
|
||||||
|
{
|
||||||
|
pub id: Uuid,
|
||||||
|
pub section_id: Uuid,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub publish_date: NaiveDate,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Insertable)]
|
||||||
|
#[table_name = "posts"]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NewPost
|
||||||
|
{
|
||||||
|
pub section_id: Uuid,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub publish_date: NaiveDate,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, AsChangeset)]
|
||||||
|
#[table_name = "posts"]
|
||||||
|
pub struct PatchPost
|
||||||
|
{
|
||||||
|
pub section_id: Option<Uuid>,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub publish_date: Option<NaiveDate>,
|
||||||
|
pub content: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Post>>
|
||||||
|
{
|
||||||
|
Ok(posts
|
||||||
|
.offset(offset_.into())
|
||||||
|
.limit(limit_.into())
|
||||||
|
.load(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't query posts."))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find(conn: &PgConnection, id_: &Uuid) -> RbOption<Post>
|
||||||
|
{
|
||||||
|
match posts.find(id_).first(conn) {
|
||||||
|
Ok(val) => Ok(Some(val)),
|
||||||
|
Err(diesel::NotFound) => Ok(None),
|
||||||
|
_ => Err(RbError::DbError("Couldn't find post.")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<Post>
|
||||||
|
{
|
||||||
|
Ok(insert_into(posts)
|
||||||
|
.values(new_post)
|
||||||
|
.get_result(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't insert post."))?)
|
||||||
|
|
||||||
|
// TODO check for conflict?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchPost) -> RbResult<Post>
|
||||||
|
{
|
||||||
|
Ok(diesel::update(posts.filter(id.eq(post_id)))
|
||||||
|
.set(patch_post)
|
||||||
|
.get_result(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't update post."))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
|
||||||
|
{
|
||||||
|
diesel::delete(posts.filter(id.eq(post_id)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't delete post."))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{RbError, RbResult},
|
||||||
|
schema::{sections, sections::dsl::*},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Queryable, Serialize)]
|
||||||
|
pub struct Section
|
||||||
|
{
|
||||||
|
pub id: Uuid,
|
||||||
|
pub title: String,
|
||||||
|
pub shortname: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub is_default: bool,
|
||||||
|
pub has_titles: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Insertable)]
|
||||||
|
#[table_name = "sections"]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NewSection
|
||||||
|
{
|
||||||
|
title: String,
|
||||||
|
pub shortname: String,
|
||||||
|
description: Option<String>,
|
||||||
|
is_default: Option<bool>,
|
||||||
|
has_titles: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, AsChangeset)]
|
||||||
|
#[table_name = "sections"]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PatchSection
|
||||||
|
{
|
||||||
|
title: Option<String>,
|
||||||
|
shortname: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
is_default: Option<bool>,
|
||||||
|
has_titles: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<Section>>
|
||||||
|
{
|
||||||
|
Ok(sections
|
||||||
|
.offset(offset_.into())
|
||||||
|
.limit(limit_.into())
|
||||||
|
.load(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't query sections."))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(conn: &PgConnection, new_post: &NewSection) -> RbResult<Section>
|
||||||
|
{
|
||||||
|
Ok(insert_into(sections)
|
||||||
|
.values(new_post)
|
||||||
|
.get_result(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't insert section."))?)
|
||||||
|
|
||||||
|
// TODO check for conflict?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(conn: &PgConnection, post_id: &Uuid, patch_post: &PatchSection) -> RbResult<Section>
|
||||||
|
{
|
||||||
|
Ok(diesel::update(sections.filter(id.eq(post_id)))
|
||||||
|
.set(patch_post)
|
||||||
|
.get_result(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't update section."))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(conn: &PgConnection, post_id: &Uuid) -> RbResult<()>
|
||||||
|
{
|
||||||
|
diesel::delete(sections.filter(id.eq(post_id)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't delete section."))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
//! Handles refresh token-related database operations.
|
||||||
|
|
||||||
|
use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{RbError, RbResult},
|
||||||
|
schema::{refresh_tokens, refresh_tokens::dsl::*},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A refresh token as stored in the database
|
||||||
|
#[derive(Queryable, Serialize)]
|
||||||
|
pub struct RefreshToken
|
||||||
|
{
|
||||||
|
pub token: Vec<u8>,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub expires_at: chrono::NaiveDateTime,
|
||||||
|
pub last_used_at: Option<chrono::NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A new refresh token to be added into the database
|
||||||
|
#[derive(Deserialize, Insertable)]
|
||||||
|
#[table_name = "refresh_tokens"]
|
||||||
|
pub struct NewRefreshToken
|
||||||
|
{
|
||||||
|
pub token: Vec<u8>,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub expires_at: chrono::NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, AsChangeset)]
|
||||||
|
#[table_name = "refresh_tokens"]
|
||||||
|
pub struct PatchRefreshToken
|
||||||
|
{
|
||||||
|
pub expires_at: Option<chrono::NaiveDateTime>,
|
||||||
|
pub last_used_at: Option<chrono::NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<RefreshToken>>
|
||||||
|
{
|
||||||
|
Ok(refresh_tokens
|
||||||
|
.offset(offset_.into())
|
||||||
|
.limit(limit_.into())
|
||||||
|
.load(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't query tokens."))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(conn: &PgConnection, new_token: &NewRefreshToken) -> RbResult<RefreshToken>
|
||||||
|
{
|
||||||
|
Ok(insert_into(refresh_tokens)
|
||||||
|
.values(new_token)
|
||||||
|
.get_result(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't insert refresh token."))?)
|
||||||
|
|
||||||
|
// TODO check for conflict?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
conn: &PgConnection,
|
||||||
|
token_: &[u8],
|
||||||
|
patch_token: &PatchRefreshToken,
|
||||||
|
) -> RbResult<RefreshToken>
|
||||||
|
{
|
||||||
|
Ok(diesel::update(refresh_tokens.filter(token.eq(token_)))
|
||||||
|
.set(patch_token)
|
||||||
|
.get_result(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't update token."))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(conn: &PgConnection, token_: &[u8]) -> RbResult<()>
|
||||||
|
{
|
||||||
|
diesel::delete(refresh_tokens.filter(token.eq(token_)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't delete token."))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the token & user data associated with the given refresh token value.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `conn` - database connection to use
|
||||||
|
/// * `token_val` - token value to search for
|
||||||
|
pub fn find_with_user(
|
||||||
|
conn: &PgConnection,
|
||||||
|
token_: &[u8],
|
||||||
|
) -> Option<(RefreshToken, super::users::User)>
|
||||||
|
{
|
||||||
|
// TODO actually check for errors here
|
||||||
|
refresh_tokens
|
||||||
|
.inner_join(crate::schema::users::dsl::users)
|
||||||
|
.filter(token.eq(token_))
|
||||||
|
.first::<(RefreshToken, super::users::User)>(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't get refresh token & user."))
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates a token's `last_used_at` column value.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `conn` - database connection to use
|
||||||
|
/// * `token_` - value of the refresh token to update
|
||||||
|
/// * `last_used_at_` - date value to update column with
|
||||||
|
///
|
||||||
|
/// **NOTE**: argument names use trailing underscores as to not conflict with Diesel's imported dsl
|
||||||
|
/// names.
|
||||||
|
pub fn update_last_used_at(
|
||||||
|
conn: &PgConnection,
|
||||||
|
token_: &[u8],
|
||||||
|
last_used_at_: chrono::NaiveDateTime,
|
||||||
|
) -> RbResult<()>
|
||||||
|
{
|
||||||
|
diesel::update(refresh_tokens.filter(token.eq(token_)))
|
||||||
|
.set(last_used_at.eq(last_used_at_))
|
||||||
|
.execute(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't update last_used_at."))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
use diesel::{prelude::*, AsChangeset, Insertable, Queryable};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{RbError, RbResult},
|
||||||
|
schema::{users, users::dsl::*},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Queryable, Serialize)]
|
||||||
|
pub struct User
|
||||||
|
{
|
||||||
|
pub id: Uuid,
|
||||||
|
pub username: String,
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub password: String,
|
||||||
|
pub blocked: bool,
|
||||||
|
pub admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Deserialize)]
|
||||||
|
#[table_name = "users"]
|
||||||
|
pub struct NewUser
|
||||||
|
{
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, AsChangeset)]
|
||||||
|
#[table_name = "users"]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PatchSection
|
||||||
|
{
|
||||||
|
username: Option<String>,
|
||||||
|
admin: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(conn: &PgConnection, offset_: u32, limit_: u32) -> RbResult<Vec<User>>
|
||||||
|
{
|
||||||
|
Ok(users
|
||||||
|
.offset(offset_.into())
|
||||||
|
.limit(limit_.into())
|
||||||
|
.load(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't query users."))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find(conn: &PgConnection, user_id: Uuid) -> Option<User>
|
||||||
|
{
|
||||||
|
users.find(user_id).first::<User>(conn).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_username(conn: &PgConnection, username_: &str) -> RbResult<User>
|
||||||
|
{
|
||||||
|
Ok(users
|
||||||
|
.filter(username.eq(username_))
|
||||||
|
.first::<User>(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't find users by username."))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a new user into the database
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `conn` - database connection to use
|
||||||
|
/// * `new_user` - user to insert
|
||||||
|
pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
||||||
|
{
|
||||||
|
let count = diesel::insert_into(users)
|
||||||
|
.values(new_user)
|
||||||
|
.execute(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't create user."))?;
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return Err(RbError::UMDuplicateUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Either create a new user or update an existing one on conflict.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `conn` - database connection to use
|
||||||
|
/// * `new_user` - user to insert/update
|
||||||
|
// pub fn create_or_update(conn: &PgConnection, new_user: &NewUser) -> RbResult<()>
|
||||||
|
// {
|
||||||
|
// diesel::insert_into(users)
|
||||||
|
// .values(new_user)
|
||||||
|
// .on_conflict(username)
|
||||||
|
// .do_update()
|
||||||
|
// .set(new_user)
|
||||||
|
// .execute(conn)
|
||||||
|
// .map_err(|_| RbError::DbError("Couldn't create or update user."))?;
|
||||||
|
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// Delete the user with the given ID.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// `conn` - database connection to use
|
||||||
|
/// `user_id` - ID of user to delete
|
||||||
|
pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
|
||||||
|
{
|
||||||
|
diesel::delete(users.filter(id.eq(user_id)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't delete user."))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Block a user given an ID.
|
||||||
|
/// In practice, this means updating the user's entry so that the `blocked` column is set to
|
||||||
|
/// `true`.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// `conn` - database connection to use
|
||||||
|
/// `user_id` - ID of user to block
|
||||||
|
pub fn block(conn: &PgConnection, user_id: Uuid) -> RbResult<()>
|
||||||
|
{
|
||||||
|
diesel::update(users.filter(id.eq(user_id)))
|
||||||
|
.set(blocked.eq(true))
|
||||||
|
.execute(conn)
|
||||||
|
.map_err(|_| RbError::DbError("Couldn't block user."))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
use rocket::{
|
||||||
|
http::Status,
|
||||||
|
request::Request,
|
||||||
|
response::{self, Responder},
|
||||||
|
serde::json::json,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RbError
|
||||||
|
{
|
||||||
|
AuthUnknownUser,
|
||||||
|
AuthBlockedUser,
|
||||||
|
AuthInvalidPassword,
|
||||||
|
AuthUnauthorized,
|
||||||
|
AuthTokenExpired,
|
||||||
|
AuthRefreshTokenExpired,
|
||||||
|
AuthInvalidRefreshToken,
|
||||||
|
AuthDuplicateRefreshToken,
|
||||||
|
AuthMissingHeader,
|
||||||
|
|
||||||
|
// UM = User Management
|
||||||
|
UMDuplicateUser,
|
||||||
|
UMUnknownUser,
|
||||||
|
|
||||||
|
DbError(&'static str),
|
||||||
|
Custom(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RbError
|
||||||
|
{
|
||||||
|
pub fn status(&self) -> Status
|
||||||
|
{
|
||||||
|
// Every entry gets its own line for easy editing later when needed
|
||||||
|
match self {
|
||||||
|
RbError::AuthUnknownUser => Status::NotFound,
|
||||||
|
RbError::AuthBlockedUser => Status::Forbidden,
|
||||||
|
RbError::AuthInvalidPassword => Status::Unauthorized,
|
||||||
|
RbError::AuthUnauthorized => Status::Unauthorized,
|
||||||
|
RbError::AuthTokenExpired => Status::Unauthorized,
|
||||||
|
RbError::AuthRefreshTokenExpired => Status::Unauthorized,
|
||||||
|
RbError::AuthInvalidRefreshToken => Status::Unauthorized,
|
||||||
|
RbError::AuthDuplicateRefreshToken => Status::Unauthorized,
|
||||||
|
RbError::AuthMissingHeader => Status::BadRequest,
|
||||||
|
|
||||||
|
RbError::UMDuplicateUser => Status::Conflict,
|
||||||
|
|
||||||
|
RbError::Custom(_) => Status::InternalServerError,
|
||||||
|
_ => Status::InternalServerError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message(&self) -> &'static str
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
RbError::AuthUnknownUser => "This user doesn't exist.",
|
||||||
|
RbError::AuthBlockedUser => "This user is blocked.",
|
||||||
|
RbError::AuthInvalidPassword => "Invalid credentials.",
|
||||||
|
RbError::AuthUnauthorized => "You are not authorized to access this resource.",
|
||||||
|
RbError::AuthTokenExpired => "This token is not valid anymore.",
|
||||||
|
RbError::AuthRefreshTokenExpired => "This refresh token is not valid anymore.",
|
||||||
|
RbError::AuthInvalidRefreshToken => "This refresh token is not valid.",
|
||||||
|
RbError::AuthDuplicateRefreshToken => {
|
||||||
|
"This refresh token has already been used. The user has been blocked."
|
||||||
|
},
|
||||||
|
RbError::AuthMissingHeader => "Missing Authorization header.",
|
||||||
|
|
||||||
|
RbError::UMDuplicateUser => "This user already exists.",
|
||||||
|
|
||||||
|
RbError::Custom(message) => message,
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> Responder<'r, 'static> for RbError
|
||||||
|
{
|
||||||
|
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static>
|
||||||
|
{
|
||||||
|
let status = self.status();
|
||||||
|
let content = json!({
|
||||||
|
"status": status.code,
|
||||||
|
"message": self.message(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO add status to response
|
||||||
|
content.respond_to(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type alias for results that can return an RbError
|
||||||
|
pub type RbResult<T> = std::result::Result<T, RbError>;
|
||||||
|
|
||||||
|
/// Type alias for optional results that can fail & return an RbError
|
||||||
|
pub type RbOption<T> = RbResult<Option<T>>;
|
|
@ -0,0 +1,115 @@
|
||||||
|
use hmac::{Hmac, NewMac};
|
||||||
|
use jwt::VerifyWithKey;
|
||||||
|
use rocket::{
|
||||||
|
http::Status,
|
||||||
|
outcome::try_outcome,
|
||||||
|
request::{FromRequest, Outcome, Request},
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use crate::{auth::jwt::Claims, errors::RbError, RbConfig};
|
||||||
|
|
||||||
|
/// Extracts an "Authorization: Bearer" string from the headers.
|
||||||
|
pub struct Bearer<'a>(&'a str);
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for Bearer<'r>
|
||||||
|
{
|
||||||
|
type Error = crate::errors::RbError;
|
||||||
|
|
||||||
|
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
|
||||||
|
{
|
||||||
|
// If the header isn't present, just forward to the next route
|
||||||
|
let header = match req.headers().get_one("Authorization") {
|
||||||
|
None => return Outcome::Forward(()),
|
||||||
|
Some(val) => val,
|
||||||
|
};
|
||||||
|
|
||||||
|
if header.starts_with("Bearer ") {
|
||||||
|
match header.get(7..) {
|
||||||
|
Some(s) => Outcome::Success(Self(s)),
|
||||||
|
None => Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Outcome::Forward(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies the provided JWT is valid.
|
||||||
|
pub struct Jwt(Claims);
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for Jwt
|
||||||
|
{
|
||||||
|
type Error = RbError;
|
||||||
|
|
||||||
|
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
|
||||||
|
{
|
||||||
|
let bearer = try_outcome!(req.guard::<Bearer>().await).0;
|
||||||
|
let config = try_outcome!(req.guard::<&State<RbConfig>>().await.map_failure(|_| (
|
||||||
|
Status::InternalServerError,
|
||||||
|
RbError::Custom("Couldn't get config guard.")
|
||||||
|
)));
|
||||||
|
|
||||||
|
let key: Hmac<Sha256> = match Hmac::new_from_slice(&config.jwt.key.as_bytes()) {
|
||||||
|
Ok(key) => key,
|
||||||
|
Err(_) => {
|
||||||
|
return Outcome::Failure((
|
||||||
|
Status::InternalServerError,
|
||||||
|
Self::Error::Custom("Failed to do Hmac thing."),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify token using key
|
||||||
|
match bearer.verify_with_key(&key) {
|
||||||
|
Ok(claims) => Outcome::Success(Self(claims)),
|
||||||
|
Err(_) => {
|
||||||
|
return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies the JWT has not expired.
|
||||||
|
pub struct User(Claims);
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for User
|
||||||
|
{
|
||||||
|
type Error = crate::errors::RbError;
|
||||||
|
|
||||||
|
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
|
||||||
|
{
|
||||||
|
let claims = try_outcome!(req.guard::<Jwt>().await).0;
|
||||||
|
|
||||||
|
// Verify key hasn't yet expired
|
||||||
|
if chrono::Utc::now().timestamp() > claims.exp {
|
||||||
|
Outcome::Failure((Status::Forbidden, Self::Error::AuthTokenExpired))
|
||||||
|
} else {
|
||||||
|
Outcome::Success(Self(claims))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies the JWT belongs to an admin.
|
||||||
|
pub struct Admin(Claims);
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for Admin
|
||||||
|
{
|
||||||
|
type Error = crate::errors::RbError;
|
||||||
|
|
||||||
|
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
|
||||||
|
{
|
||||||
|
let user = try_outcome!(req.guard::<User>().await).0;
|
||||||
|
|
||||||
|
if user.admin {
|
||||||
|
Outcome::Success(Self(user))
|
||||||
|
} else {
|
||||||
|
Outcome::Failure((Status::Unauthorized, RbError::AuthUnauthorized))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
// This needs to be explicitely included before diesel is imported to make sure
|
||||||
|
// compilation succeeds in the release Docker image.
|
||||||
|
extern crate openssl;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate rocket;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate diesel_migrations;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate diesel;
|
||||||
|
|
||||||
|
use figment::{
|
||||||
|
providers::{Env, Format, Yaml},
|
||||||
|
Figment,
|
||||||
|
};
|
||||||
|
#[cfg(any(feature = "web", feature = "docs"))]
|
||||||
|
use rocket::fs;
|
||||||
|
use rocket::{
|
||||||
|
fairing::AdHoc,
|
||||||
|
http::Status,
|
||||||
|
serde::json::{json, Value},
|
||||||
|
Build, Orbit, Request, Rocket,
|
||||||
|
};
|
||||||
|
use rocket_sync_db_pools::database;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
mod admin;
|
||||||
|
pub mod auth;
|
||||||
|
pub mod db;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod guards;
|
||||||
|
pub mod posts;
|
||||||
|
pub(crate) mod schema;
|
||||||
|
pub mod sections;
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||||
|
|
||||||
|
#[database("postgres_rb")]
|
||||||
|
pub struct RbDbConn(diesel::PgConnection);
|
||||||
|
|
||||||
|
#[catch(default)]
|
||||||
|
fn default_catcher(status: Status, _: &Request) -> Value
|
||||||
|
{
|
||||||
|
json!({"status": status.code, "message": ""})
|
||||||
|
}
|
||||||
|
|
||||||
|
embed_migrations!();
|
||||||
|
|
||||||
|
async fn run_db_migrations(rocket: Rocket<Build>) -> Result<Rocket<Build>, Rocket<Build>>
|
||||||
|
{
|
||||||
|
let conn = RbDbConn::get_one(&rocket)
|
||||||
|
.await
|
||||||
|
.expect("database connection");
|
||||||
|
conn.run(|c| match embedded_migrations::run(c) {
|
||||||
|
Ok(()) => Ok(rocket),
|
||||||
|
Err(_) => Err(rocket),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_admin_user<'a>(rocket: &'a Rocket<Orbit>)
|
||||||
|
{
|
||||||
|
let config = rocket.state::<RbConfig>().expect("RbConfig instance");
|
||||||
|
let admin_user = config.admin_user.clone();
|
||||||
|
let admin_pass = config.admin_pass.clone();
|
||||||
|
|
||||||
|
let conn = RbDbConn::get_one(&rocket)
|
||||||
|
.await
|
||||||
|
.expect("database connection");
|
||||||
|
conn.run(move |c| {
|
||||||
|
admin::create_admin_user(c, &admin_user, &admin_pass).expect("failed to create admin user")
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
pub struct RbJwtConf
|
||||||
|
{
|
||||||
|
key: String,
|
||||||
|
refresh_token_size: usize,
|
||||||
|
refresh_token_expire: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct RbConfig
|
||||||
|
{
|
||||||
|
admin_user: String,
|
||||||
|
admin_pass: String,
|
||||||
|
jwt: RbJwtConf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[launch]
|
||||||
|
fn rocket() -> _
|
||||||
|
{
|
||||||
|
let figment = Figment::from(rocket::config::Config::default())
|
||||||
|
.merge(Yaml::file("Rb.yaml").nested())
|
||||||
|
.merge(Env::prefixed("RB_").global());
|
||||||
|
|
||||||
|
// This mut is necessary when the "docs" or "web" feature is enabled, as these further modify
|
||||||
|
// the instance variable
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut instance = rocket::custom(figment)
|
||||||
|
.attach(RbDbConn::fairing())
|
||||||
|
.attach(AdHoc::try_on_ignite(
|
||||||
|
"Run database migrations",
|
||||||
|
run_db_migrations,
|
||||||
|
))
|
||||||
|
// .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user))
|
||||||
|
.attach(AdHoc::config::<RbConfig>())
|
||||||
|
.register("/", catchers![default_catcher])
|
||||||
|
.mount(
|
||||||
|
"/api/auth",
|
||||||
|
routes![auth::already_logged_in, auth::login, auth::refresh_token,],
|
||||||
|
)
|
||||||
|
.mount(
|
||||||
|
"/api/admin",
|
||||||
|
routes![admin::create_user, admin::get_user_info],
|
||||||
|
)
|
||||||
|
.mount("/api/sections", routes![sections::create_section])
|
||||||
|
.mount("/api/posts", routes![posts::get, posts::create]);
|
||||||
|
|
||||||
|
// It's weird that this is allowed, but the line on its own isn't
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
{
|
||||||
|
instance = instance.mount(
|
||||||
|
"/",
|
||||||
|
fs::FileServer::new(
|
||||||
|
"/var/www/html/web",
|
||||||
|
fs::Options::Index | fs::Options::NormalizeDirs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "docs")]
|
||||||
|
{
|
||||||
|
instance = instance.mount(
|
||||||
|
"/docs",
|
||||||
|
fs::FileServer::new(
|
||||||
|
"/var/www/html/docs",
|
||||||
|
fs::Options::Index | fs::Options::NormalizeDirs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
instance
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db,
|
||||||
|
errors::{RbOption, RbResult},
|
||||||
|
guards::Admin,
|
||||||
|
RbDbConn,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[get("/?<offset>&<limit>")]
|
||||||
|
pub async fn get(conn: RbDbConn, offset: u32, limit: u32) -> RbResult<Json<Vec<db::Post>>>
|
||||||
|
{
|
||||||
|
Ok(Json(
|
||||||
|
conn.run(move |c| db::posts::get(c, offset, limit)).await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/", data = "<new_post>")]
|
||||||
|
pub async fn create(
|
||||||
|
_admin: Admin,
|
||||||
|
conn: RbDbConn,
|
||||||
|
new_post: Json<db::NewPost>,
|
||||||
|
) -> RbResult<Json<db::Post>>
|
||||||
|
{
|
||||||
|
Ok(Json(
|
||||||
|
conn.run(move |c| db::posts::create(c, &new_post.into_inner()))
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/<id>")]
|
||||||
|
pub async fn find(conn: RbDbConn, id: uuid::Uuid) -> RbOption<Json<db::Post>>
|
||||||
|
{
|
||||||
|
Ok(conn
|
||||||
|
.run(move |c| db::posts::find(c, &id))
|
||||||
|
.await?
|
||||||
|
.and_then(|p| Some(Json(p))))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("/<id>", data = "<patch_post>")]
|
||||||
|
pub async fn patch(
|
||||||
|
_admin: Admin,
|
||||||
|
conn: RbDbConn,
|
||||||
|
id: uuid::Uuid,
|
||||||
|
patch_post: Json<db::PatchPost>,
|
||||||
|
) -> RbResult<Json<db::Post>>
|
||||||
|
{
|
||||||
|
Ok(Json(
|
||||||
|
conn.run(move |c| db::posts::update(c, &id, &patch_post.into_inner()))
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/<id>")]
|
||||||
|
pub async fn delete(_admin: Admin, conn: RbDbConn, id: uuid::Uuid) -> RbResult<()>
|
||||||
|
{
|
||||||
|
Ok(conn.run(move |c| db::posts::delete(c, &id)).await?)
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
pub fn yeet() -> String {
|
|
||||||
String::from("yeet")
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
// This needs to be explicitely included before diesel is imported to make sure
|
|
||||||
// compilation succeeds
|
|
||||||
extern crate openssl;
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate rocket;
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate diesel_migrations;
|
|
||||||
|
|
||||||
use rocket::{fairing::AdHoc, Build, Rocket};
|
|
||||||
use rocket_sync_db_pools::{database, diesel};
|
|
||||||
|
|
||||||
embed_migrations!();
|
|
||||||
|
|
||||||
#[database("postgres_rb")]
|
|
||||||
pub struct RbDbConn(diesel::PgConnection);
|
|
||||||
|
|
||||||
async fn run_db_migrations(rocket: Rocket<Build>) -> Result<Rocket<Build>, Rocket<Build>> {
|
|
||||||
let conn = RbDbConn::get_one(&rocket)
|
|
||||||
.await
|
|
||||||
.expect("database connection");
|
|
||||||
conn.run(|c| match embedded_migrations::run(c) {
|
|
||||||
Ok(()) => Ok(rocket),
|
|
||||||
Err(_) => Err(rocket),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[launch]
|
|
||||||
fn rocket() -> _ {
|
|
||||||
rocket::build()
|
|
||||||
.attach(RbDbConn::fairing())
|
|
||||||
.attach(AdHoc::try_on_ignite(
|
|
||||||
"Run database migrations",
|
|
||||||
run_db_migrations,
|
|
||||||
))
|
|
||||||
}
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
table! {
|
||||||
|
posts (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
section_id -> Uuid,
|
||||||
|
title -> Nullable<Varchar>,
|
||||||
|
publish_date -> Date,
|
||||||
|
content -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
refresh_tokens (token) {
|
||||||
|
token -> Bytea,
|
||||||
|
user_id -> Uuid,
|
||||||
|
expires_at -> Timestamp,
|
||||||
|
last_used_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
sections (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
title -> Varchar,
|
||||||
|
shortname -> Varchar,
|
||||||
|
description -> Nullable<Text>,
|
||||||
|
is_default -> Bool,
|
||||||
|
has_titles -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
users (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
username -> Varchar,
|
||||||
|
password -> Text,
|
||||||
|
blocked -> Bool,
|
||||||
|
admin -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
joinable!(posts -> sections (section_id));
|
||||||
|
joinable!(refresh_tokens -> users (user_id));
|
||||||
|
|
||||||
|
allow_tables_to_appear_in_same_query!(posts, refresh_tokens, sections, users,);
|
|
@ -0,0 +1,25 @@
|
||||||
|
//! This module handles management of site sections (aka blogs).
|
||||||
|
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
|
||||||
|
use crate::{db, errors::RbResult, guards::Admin, RbDbConn};
|
||||||
|
|
||||||
|
/// Route for creating a new section.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `_admin` - guard ensuring user is admin
|
||||||
|
/// * `conn` - guard providing a connection to the database
|
||||||
|
/// * `new_section` - Json-encoded NewSection object
|
||||||
|
#[post("/", data = "<new_section>")]
|
||||||
|
pub async fn create_section(
|
||||||
|
_admin: Admin,
|
||||||
|
conn: RbDbConn,
|
||||||
|
new_section: Json<db::NewSection>,
|
||||||
|
) -> RbResult<Json<db::Section>>
|
||||||
|
{
|
||||||
|
Ok(Json(
|
||||||
|
conn.run(move |c| db::sections::create(c, &new_section.into_inner()))
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class RbClient:
|
||||||
|
def __init__(self, username = "admin", password = "password", base_url = "http://localhost:8000/api"):
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
self.jwt = None
|
||||||
|
self.refresh_token = None
|
||||||
|
|
||||||
|
def _login(self):
|
||||||
|
r = requests.post(f"{self.base_url}/auth/login", json={
|
||||||
|
"username": self.username,
|
||||||
|
"password": self.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
print(r.text)
|
||||||
|
raise Exception("Couldn't login")
|
||||||
|
|
||||||
|
res = r.json()
|
||||||
|
self.jwt = res["token"]
|
||||||
|
self.refresh_token = res["refreshToken"]
|
||||||
|
|
||||||
|
def _refresh(self):
|
||||||
|
r = requests.post(f"{self.base_url}/auth/refresh", json={"refreshToken": self.refresh_token})
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise Exception("Couldn't refresh")
|
||||||
|
|
||||||
|
res = r.json()
|
||||||
|
self.jwt = res["token"]
|
||||||
|
self.refresh_token = res["refreshToken"]
|
||||||
|
|
||||||
|
def _request(self, type_, url, retry=2, *args, **kwargs):
|
||||||
|
if self.jwt:
|
||||||
|
headers = kwargs.get("headers", {})
|
||||||
|
headers["Authorization"] = f"Bearer {self.jwt}"
|
||||||
|
kwargs["headers"] = headers
|
||||||
|
print(kwargs["headers"])
|
||||||
|
|
||||||
|
r = requests.request(type_, url, *args, **kwargs)
|
||||||
|
|
||||||
|
if r.status_code != 200 and retry > 0:
|
||||||
|
if self.refresh_token:
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._login()
|
||||||
|
|
||||||
|
r = self._request(type_, url, *args, **kwargs, retry=retry - 1)
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
def get(self, url, *args, **kwargs):
|
||||||
|
return self._request("GET", f"{self.base_url}{url}", *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, url, *args, **kwargs):
|
||||||
|
return self._request("POST", f"{self.base_url}{url}", *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
client = RbClient()
|
||||||
|
|
||||||
|
# print(client.get("/admin/users").json())
|
||||||
|
client.post("/sections", json={
|
||||||
|
"title": "this is a title"
|
||||||
|
})
|
|
@ -1,18 +1,5 @@
|
||||||
# build output
|
node_modules
|
||||||
dist
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
node_modules/
|
|
||||||
.snowpack/
|
|
||||||
|
|
||||||
# logs
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# environment variables
|
|
||||||
.env
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# macOS-specific files
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
## force pnpm to hoist
|
|
||||||
shamefully-hoist = true
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["johnsoncodehk.volar"]
|
||||||
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
# Welcome to [Astro](https://astro.build)
|
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
|
||||||
|
|
||||||
## 🚀 Project Structure
|
|
||||||
|
|
||||||
Inside of your Astro project, you'll see the following folders and files:
|
|
||||||
|
|
||||||
```
|
|
||||||
/
|
|
||||||
├── public/
|
|
||||||
│ ├── robots.txt
|
|
||||||
│ └── favicon.ico
|
|
||||||
├── src/
|
|
||||||
│ ├── components/
|
|
||||||
│ │ └── Tour.astro
|
|
||||||
│ └── pages/
|
|
||||||
│ └── index.astro
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
|
||||||
|
|
||||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
|
||||||
|
|
||||||
Any static assets, like images, can be placed in the `public/` directory.
|
|
||||||
|
|
||||||
## 🧞 Commands
|
|
||||||
|
|
||||||
All commands are run from the root of the project, from a terminal:
|
|
||||||
|
|
||||||
| Command | Action |
|
|
||||||
|:----------------|:--------------------------------------------|
|
|
||||||
| `npm install` | Installs dependencies |
|
|
||||||
| `npm start` | Starts local dev server at `localhost:3000` |
|
|
||||||
| `npm run build` | Build your production site to `./dist/` |
|
|
||||||
|
|
||||||
## 👀 Want to learn more?
|
|
||||||
|
|
||||||
Feel free to check [our documentation](https://github.com/snowpackjs/astro) or jump into our [Discord server](https://astro.build/chat).
|
|
|
@ -1,18 +0,0 @@
|
||||||
export default {
|
|
||||||
// projectRoot: '.', // Where to resolve all URLs relative to. Useful if you have a monorepo project.
|
|
||||||
// pages: './src/pages', // Path to Astro components, pages, and data
|
|
||||||
// dist: './dist', // When running `astro build`, path to final static output
|
|
||||||
// public: './public', // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that don’t need processing.
|
|
||||||
buildOptions: {
|
|
||||||
// site: 'http://example.com', // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs.
|
|
||||||
sitemap: true, // Generate sitemap (set to "false" to disable)
|
|
||||||
},
|
|
||||||
devOptions: {
|
|
||||||
// hostname: 'localhost', // The hostname to run the dev server on.
|
|
||||||
// port: 3000, // The port to run the dev server on.
|
|
||||||
// tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js'
|
|
||||||
},
|
|
||||||
renderers: [
|
|
||||||
"@astrojs/renderer-svelte"
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,13 +1,20 @@
|
||||||
{
|
{
|
||||||
"name": "@example/starter",
|
"name": "rusty-bever",
|
||||||
"version": "0.0.1",
|
"version": "0.0.0",
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "astro dev",
|
"dev": "vite",
|
||||||
"build": "astro build"
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"serve": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.2.16"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"astro": "0.19.0-next.2",
|
"@types/node": "^16.10.3",
|
||||||
"@astrojs/renderer-svelte": "^0.1.1"
|
"@vitejs/plugin-vue": "^1.9.3",
|
||||||
|
"miragejs": "^0.1.42",
|
||||||
|
"typescript": "^4.4.3",
|
||||||
|
"vite": "^2.6.4",
|
||||||
|
"vue-tsc": "^0.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
<svg width="193" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<style>
|
|
||||||
#flame { fill: #FF5D01; }
|
|
||||||
#a { fill: #000014; }
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
#a { fill: #fff; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M131.496 18.929c1.943 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53L99.746 60.56a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.224 180.224 0 00-52.01 17.557l43.52-142.281c1.989-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.085 1.157a16 16 0 016.488 4.806z" fill="url(#paint0_linear)"/>
|
|
||||||
<path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M136.678 180.151c-7.14 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.962 10.367-1.962 13.902 0 0-1.055 17.355 11.016 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.973-19.87 5.977-3.79 12.616-8.001 17.192-16.449a31.013 31.013 0 003.744-14.82c0-3.299-.513-6.479-1.463-9.463z" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -1,11 +0,0 @@
|
||||||
<svg width="256" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<style>
|
|
||||||
#flame { fill: #FF5D01; }
|
|
||||||
#a { fill: #000014; }
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
#a { fill: #fff; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" />
|
|
||||||
<path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.2 KiB |
|
@ -1,2 +0,0 @@
|
||||||
User-agent: *
|
|
||||||
Disallow: /
|
|
|
@ -1,28 +0,0 @@
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
|
|
||||||
font-size: 1rem;
|
|
||||||
--user-font-scale: 1rem - 16px;
|
|
||||||
font-size: clamp(0.875rem, 0.4626rem + 1.0309vw + var(--user-font-scale), 1.125rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
padding: 4rem 2rem;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: grid;
|
|
||||||
justify-content: center;
|
|
||||||
background: #f9fafb;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background: #111827;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
:root {
|
|
||||||
--font-mono: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono',
|
|
||||||
'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace;
|
|
||||||
--color-light: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--color-light: #1f2937;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
header > div {
|
|
||||||
font-size: clamp(2rem, -0.4742rem + 6.1856vw, 2.75rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
header > div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
header img {
|
|
||||||
width: 2em;
|
|
||||||
height: 2.667em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: clamp(1.5rem, 1rem + 1.25vw, 2rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.counter {
|
|
||||||
display: grid;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
gap: 1em;
|
|
||||||
font-size: 2rem;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.counter > pre {
|
|
||||||
text-align: center;
|
|
||||||
min-width: 3ch;
|
|
||||||
}
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
// This starter template is using Vue 3 <script setup> SFCs
|
||||||
|
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
|
||||||
|
import HelloWorld from './components/HelloWorld.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<img alt="Vue logo" src="./assets/logo.png" />
|
||||||
|
<HelloWorld msg="Hello Vue 3 + TypeScript + Vite" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-align: center;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
</style>
|
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
|
@ -0,0 +1,68 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps<{ msg: string }>()
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
let test = ref("yeet")
|
||||||
|
|
||||||
|
fetch("/api/users").then(
|
||||||
|
res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
console.log("ah chucks")
|
||||||
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
).then(
|
||||||
|
json => test.value = json
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>{{ msg }}</h1>
|
||||||
|
|
||||||
|
<p>{{ test }}</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Recommended IDE setup:
|
||||||
|
<a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
|
||||||
|
+
|
||||||
|
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>See <code>README.md</code> for more information.</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://vitejs.dev/guide/features.html" target="_blank">
|
||||||
|
Vite Docs
|
||||||
|
</a>
|
||||||
|
|
|
||||||
|
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button type="button" @click="count++">count is: {{ count }}</button>
|
||||||
|
<p>
|
||||||
|
Edit
|
||||||
|
<code>components/HelloWorld.vue</code> to test hot module replacement.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
a {
|
||||||
|
color: #42b983;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin: 0 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #304455;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,17 +0,0 @@
|
||||||
<script>
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
function add() {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function subtract() {
|
|
||||||
count -= 1;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="svelte" class="counter">
|
|
||||||
<button on:click={subtract}>-</button>
|
|
||||||
<pre>{ count }</pre>
|
|
||||||
<button on:click={add}>+</button>
|
|
||||||
</div>
|
|
|
@ -1,85 +0,0 @@
|
||||||
---
|
|
||||||
import { Markdown } from 'astro/components';
|
|
||||||
---
|
|
||||||
<article>
|
|
||||||
<div class="banner">
|
|
||||||
<p><strong>🧑🚀 Seasoned astronaut?</strong> Delete this file. Have fun!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<Markdown>
|
|
||||||
## 🚀 Project Structure
|
|
||||||
|
|
||||||
Inside of your Astro project, you'll see the following folders and files:
|
|
||||||
|
|
||||||
```
|
|
||||||
/
|
|
||||||
├── public/
|
|
||||||
│ ├── robots.txt
|
|
||||||
│ └── favicon.ico
|
|
||||||
├── src/
|
|
||||||
│ ├── components/
|
|
||||||
│ │ └── Tour.astro
|
|
||||||
│ └── pages/
|
|
||||||
│ └── index.astro
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory.
|
|
||||||
Each page is exposed as a route based on its file name.
|
|
||||||
|
|
||||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
|
||||||
|
|
||||||
Any static assets, like images, can be placed in the `public/` directory.
|
|
||||||
</Markdown>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>👀 Want to learn more?</h2>
|
|
||||||
<p>Feel free to check <a href="https://github.com/snowpackjs/astro">our documentation</a> or jump into our <a href="https://astro.build/chat">Discord server</a>.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
article {
|
|
||||||
padding-top: 2em;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
margin-top: 2em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1em;
|
|
||||||
max-width: 70ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
background: var(--color-light);
|
|
||||||
padding: 1em 1.5em;
|
|
||||||
padding-left: 0.75em;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre,
|
|
||||||
code {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
background: var(--color-light);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
padding: 1em 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree {
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
code:not(.tree) {
|
|
||||||
padding: 0.125em;
|
|
||||||
margin: 0 -0.125em;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import { DefineComponent } from 'vue'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<h1>huh</h1>
|
|
||||||
<p>lol</p>
|
|
||||||
<slot />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
// @ts-ignore
|
||||||
|
import { makeServer } from "./server"
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
makeServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
|
@ -1,44 +0,0 @@
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<ul id="nav-bar">
|
|
||||||
<li class="nav-bar-item"><a href="/home">Home</a></li>
|
|
||||||
<li class="nav-bar-item"><a href="/blog">Blog</a></li>
|
|
||||||
<li class="nav-bar-item"><a href="/microblog">Microblog</a></li>
|
|
||||||
<li class="nav-bar-item"><a href="/devlogs">Devlogs</a></li>
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
ul#nav-bar {
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 200px;
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
border: 1px solid #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul#nav-bar li {
|
|
||||||
text-align: center;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.nav-bar-item a {
|
|
||||||
display: block;
|
|
||||||
color: #000;
|
|
||||||
padding: 8px 16px;
|
|
||||||
text-decoration: none;
|
|
||||||
border: 1px solid #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.nav-bar-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.nav-bar-item a:hover {
|
|
||||||
background-color: #555;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
// src/server.js
|
||||||
|
import { createServer, Model } from "miragejs"
|
||||||
|
|
||||||
|
export function makeServer({ environment = "development" } = {}) {
|
||||||
|
let server = createServer({
|
||||||
|
environment,
|
||||||
|
|
||||||
|
models: {
|
||||||
|
user: Model,
|
||||||
|
},
|
||||||
|
|
||||||
|
seeds(server) {
|
||||||
|
server.create("user", { name: "Bob" })
|
||||||
|
server.create("user", { name: "Alice" })
|
||||||
|
},
|
||||||
|
|
||||||
|
routes() {
|
||||||
|
this.namespace = "api"
|
||||||
|
|
||||||
|
this.get("/users", (schema) => {
|
||||||
|
return schema.users.all()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
|
@ -1,3 +1,15 @@
|
||||||
{
|
{
|
||||||
"moduleResolution": "node"
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["esnext", "dom"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()]
|
||||||
|
})
|
5511
web/yarn.lock
5511
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue