Compare commits

..

73 Commits

Author SHA1 Message Date
Jef Roosens e2003442e2
Main now ignores unused mut 2021-10-12 17:10:51 +02:00
Jef Roosens 19a21b8cdf
Added docs & web FileServer routes 2021-10-12 17:05:21 +02:00
Jef Roosens ce97f36c18
Removed unnecessary build.rs 2021-10-12 16:47:05 +02:00
Jef Roosens a107d9e283
Smoothly wrote post find function 2021-10-12 16:41:14 +02:00
Jef Roosens d013bd60bd
Some routes for managing posts 2021-10-11 19:47:44 +02:00
Jef Roosens 449c20fac2
Added API specification file 2021-10-11 17:39:20 +02:00
Jef Roosens 924feb662d
Initial draft of Alpine-based Dockerfile 2021-10-10 16:33:07 +02:00
Jef Roosens 3e9c5e4fe7
Some changes 2021-10-10 15:46:19 +02:00
Jef Roosens 5cbb1c1a97
Fixed site not building for production 2021-10-10 09:53:05 +02:00
Jef Roosens 8e433c3250
Replaced web folder 2021-10-10 09:28:21 +02:00
Jef Roosens 061a9d9bc6
Some more stuff 2021-10-09 22:01:52 +02:00
Jef Roosens 18f717685a
Initialized Vue 3 project using Vite 2021-10-07 18:29:01 +02:00
Jef Roosens f2a0401601
Some route boilerplate for posts 2021-09-28 10:16:20 +02:00
Jef Roosens 769d7a32de
Some more db boilerplate 2021-09-26 19:02:17 +02:00
Jef Roosens f6e9039b59
Wrote part of posts db boilerplate 2021-09-26 18:36:15 +02:00
Jef Roosens 0da8eb127c
Tried to add docs & frontend as features 2021-09-24 16:18:41 +02:00
Jef Roosens cae6632cf6
Added roadmap file 2021-09-24 12:16:55 +02:00
Jef Roosens 1441e3e601
Merge branch 'sections-backend' into develop 2021-09-23 15:32:59 +02:00
Jef Roosens 548dd0d022
Tried to add MirageJS but failed 2021-09-23 15:30:39 +02:00
Jef Roosens 6d83c18036
Documented entire db section 2021-09-15 11:36:40 +02:00
Jef Roosens 3e7612a9a8
Added create section endpoint 2021-09-13 22:15:38 +02:00
Jef Roosens 8534090f0f
Wrote some database boilerplate 2021-09-13 17:35:06 +02:00
Jef Roosens 211e31a008
Wrote first draft of sections database scheme 2021-09-13 17:18:33 +02:00
Jef Roosens 3d024db2e9
This compilation is gonna kill me 2021-09-05 11:19:18 +02:00
Jef Roosens a295237863
Some random stuff tbh 2021-09-05 09:59:16 +02:00
Jef Roosens a6b1b0ff76
Added mimalloc as allocator 2021-09-03 16:31:06 +02:00
Jef Roosens 505907d3a1
Default catcher now returns json 2021-09-01 17:29:39 +02:00
Jef Roosens f50008ff99
Fixed guard still using env var for jwt key 2021-09-01 16:18:48 +02:00
Jef Roosens fb2a6126fe
Moved all database code to db module 2021-09-01 12:53:00 +02:00
Jef Roosens 2cc4d53961
Moved JWT config to config file 2021-08-30 15:28:47 +02:00
Jef Roosens 3cf7661faf
Switched to yaml-based config 2021-08-30 14:27:54 +02:00
Jef Roosens 02011e04ce
Moved some JWT db commands to db 2021-08-29 21:15:10 +02:00
Jef Roosens 1378219fe5
Moved admin functions to correct module 2021-08-29 20:41:03 +02:00
Jef Roosens 87d8d8ff0c
Switched to binary-only project 2021-08-29 20:30:33 +02:00
Jef Roosens c64eaf2ff5
Merge branch 'revamp-errors' into develop 2021-08-29 19:56:04 +02:00
Jef Roosens 27b904b3f5
Pleased the linters 2021-08-29 19:07:36 +02:00
Jef Roosens 6858e9da62
Restructured auth 2021-08-29 19:04:06 +02:00
Jef Roosens dd51d107e3
Renamed errors; changed Responser implementation 2021-08-28 22:06:09 +02:00
Jef Roosens b61b329996
Merge branch 'develop' into revamp-errors 2021-08-28 21:16:44 +02:00
Jef Roosens 7d11d4ad17
Merge branch 'makefile' into develop 2021-08-28 21:15:53 +02:00
Jef Roosens de8be87036
Added some comments 2021-08-28 16:37:32 +02:00
Jef Roosens 055d1f9e8d
Added dumb-init; changed some stuff 2021-08-28 14:20:51 +02:00
Jef Roosens 85cfe6290c
Almost working libpq 2021-08-28 13:53:13 +02:00
Jef Roosens c912c0aa0b
Beginning of Makefile 2021-08-28 12:41:43 +02:00
Jef Roosens a100ea52a0
First draft stuff 2021-08-27 08:50:48 +02:00
Jef Roosens 1ee9b78d81
Merge branch 'develop' of git.hackbever.be:Chewing_Bever/rusty-bever into develop 2021-08-23 12:01:26 +02:00
Jef Roosens a8cd8618a3
Added lifetime thingy 2021-08-23 12:01:04 +02:00
Jef Roosens 456c947ecd
Added a single lifetime parameter 2021-08-23 08:17:06 +02:00
Jef Roosens 159da81b8d
Started on user management routes 2021-08-22 22:35:07 +02:00
Jef Roosens b4fc6fe2c0
Small changes 2021-08-22 22:01:27 +02:00
Jef Roosens 7afdd02712
Cleaned up Cargo.toml 2021-08-22 18:57:47 +02:00
Jef Roosens 16ddc9aecd
Configured Rustfmt 2021-08-22 16:45:01 +02:00
Jef Roosens b13b760e2f
Completely restructured codebase 2021-08-22 16:24:59 +02:00
Jef Roosens b45c93cdc9
Added admin users route 2021-08-22 15:50:58 +02:00
Jef Roosens d7333373bb
Token refresh works! 2021-08-22 13:41:03 +02:00
Jef Roosens 7dffbb9597
First draft of token refresh 2021-08-22 10:42:58 +02:00
Jef Roosens badf68e579
Merge branch 'develop' of git.hackbever.be:Chewing_Bever/rusty-bever into develop 2021-08-21 23:02:50 +02:00
Jef Roosens cc7a668ab0
Further split guards 2021-08-21 23:02:17 +02:00
Jef Roosens 89851a2018
Further split guards 2021-08-21 22:41:38 +02:00
Jef Roosens dab90bc4a9
Separated JWT header into own guard 2021-08-21 22:21:42 +02:00
Jef Roosens 35fb38de9e
Added Admin & User guards 2021-08-21 21:42:36 +02:00
Jef Roosens 210eeee7b6
Added default values to admin password 2021-08-21 20:22:19 +02:00
Jef Roosens 13259249fd
First successful JWT token request achieved 2021-08-21 18:51:29 +02:00
Jef Roosens 0d4d96d761
Added very basic admin user creation 2021-08-21 18:05:16 +02:00
Jef Roosens ac762a3c31
Mounted auth routes 2021-08-21 17:03:10 +02:00
Jef Roosens 9309ec77fb
First JWT login implementation 2021-08-21 16:45:41 +02:00
Jef Roosens 7a97b99bd6
Added some basic error handling 2021-08-21 15:58:51 +02:00
Jef Roosens 6782fecc0d
Started JWT token generation 2021-08-21 13:46:41 +02:00
Jef Roosens d90dbcdc2a
Some broken shit 2021-08-20 23:09:22 +02:00
Jef Roosens 1c524f181f
Removed role system 2021-08-20 22:25:21 +02:00
Jef Roosens 5e86133651
Started some auth stuff 2021-08-20 16:55:56 +02:00
Jef Roosens eefaf7acaa
First draft of refresh_tokens table 2021-08-20 14:46:19 +02:00
Jef Roosens 4ccee64323
Started writing auth sql schema 2021-08-20 14:06:01 +02:00
64 changed files with 3118 additions and 5148 deletions

3
.cargo/config 100644
View File

@ -0,0 +1,3 @@
# vim: ft=toml
[build]
target-dir = "out/target"

14
.dockerignore 100644
View File

@ -0,0 +1,14 @@
*
!.cargo/
!Cargo.lock
!Cargo.toml
!Makefile
!migrations
!Rb.yaml
!rustfmt.toml
!src
!tests
!web
web/node_modules

7
.editorconfig 100644
View File

@ -0,0 +1,7 @@
root = true
[*]
end_of_line = lf
insert_final_newline = false
indent_style = space
indent_size = 4

5
.gitignore vendored
View File

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

41
API.md 100644
View File

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

291
Cargo.lock generated
View File

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

View File

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

66
Dockerfile 100644
View File

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

140
Makefile 100644
View File

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

34
ROADMAP.md 100644
View File

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

43
Rb.yaml 100644
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS users, refresh_tokens CASCADE;

View File

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

View File

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

View File

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

69
rustfmt.toml 100644
View File

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

58
src/admin.rs 100644
View File

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

118
src/auth/jwt.rs 100644
View File

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

68
src/auth/mod.rs 100644
View File

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

36
src/auth/pass.rs 100644
View File

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

12
src/db/mod.rs 100644
View File

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

85
src/db/posts.rs 100644
View File

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

79
src/db/sections.rs 100644
View File

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

122
src/db/tokens.rs 100644
View File

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

131
src/db/users.rs 100644
View File

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

94
src/errors.rs 100644
View File

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

115
src/guards.rs 100644
View File

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

146
src/main.rs 100644
View File

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

58
src/posts.rs 100644
View File

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

View File

@ -1,3 +0,0 @@
pub fn yeet() -> String {
String::from("yeet")
}

View File

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

44
src/schema.rs 100644
View File

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

25
src/sections.rs 100644
View File

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

71
tests/admin.py 100644
View File

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

21
web/.gitignore vendored
View File

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

View File

@ -1,2 +0,0 @@
## force pnpm to hoist
shamefully-hoist = true

3
web/.vscode/extensions.json vendored 100644
View File

@ -0,0 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
}

View File

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

View File

@ -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 dont 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"
],
};

13
web/index.html 100644
View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
User-agent: *
Disallow: /

View File

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

View File

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

21
web/src/App.vue 100644
View File

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

View File

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

View File

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

View File

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

8
web/src/env.d.ts vendored 100644
View File

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

View File

@ -1,7 +0,0 @@
<html>
<body>
<h1>huh</h1>
<p>lol</p>
<slot />
</body>
</html>

10
web/src/main.ts 100644
View File

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

View File

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

27
web/src/server.js 100644
View File

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

View File

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

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
})

File diff suppressed because it is too large Load Diff