custosm avatars and banners

This commit is contained in:
Tyler Murphy 2023-01-31 22:21:19 -05:00
parent 1250cdde2b
commit d912531480
41 changed files with 576 additions and 23 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
xssbook.db
/public/avatar/custom/*

311
Cargo.lock generated
View file

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
@ -97,6 +103,12 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "bit_field"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -118,6 +130,18 @@ version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "bytemuck"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c041d3eab048880cb0b86b256447da3f18859a163c3b8d8893f4e6368abe6393"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.3.0"
@ -136,6 +160,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "cookie"
version = "0.16.2"
@ -156,6 +186,49 @@ dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
dependencies = [
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.14"
@ -165,6 +238,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -198,6 +277,27 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "either"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "exr"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eb5f255b5980bb0c8cf676b675d1a99be40f316881444f44e0462eaf5df5ded"
dependencies = [
"bit_field",
"flume",
"half",
"lebe",
"miniz_oxide",
"smallvec",
"threadpool",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
@ -210,6 +310,29 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "flate2"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.10.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"pin-project",
"spin",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -347,8 +470,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
name = "gif"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
@ -369,6 +504,15 @@ dependencies = [
"smallvec",
]
[[package]]
name = "half"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -484,12 +628,40 @@ dependencies = [
"want",
]
[[package]]
name = "image"
version = "0.24.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"exr",
"gif",
"jpeg-decoder",
"num-rational",
"num-traits",
"png",
"scoped_threadpool",
"tiff",
]
[[package]]
name = "itoa"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]]
name = "jpeg-decoder"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
dependencies = [
"rayon",
]
[[package]]
name = "js-sys"
version = "0.3.60"
@ -505,6 +677,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.139"
@ -562,6 +740,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.16"
@ -578,6 +765,15 @@ dependencies = [
"unicase",
]
[[package]]
name = "miniz_oxide"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.8.5"
@ -590,6 +786,15 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "nanorand"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
"getrandom",
]
[[package]]
name = "no-std-compat"
version = "0.4.1"
@ -618,6 +823,36 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.15.0"
@ -707,6 +942,18 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "png"
version = "0.17.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
dependencies = [
"bitflags",
"crc32fast",
"flate2",
"miniz_oxide",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -786,6 +1033,28 @@ dependencies = [
"bitflags",
]
[[package]]
name = "rayon"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
@ -821,6 +1090,12 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
[[package]]
name = "scoped_threadpool"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -933,6 +1208,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "spin"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09"
dependencies = [
"lock_api",
]
[[package]]
name = "syn"
version = "1.0.107"
@ -979,6 +1263,26 @@ dependencies = [
"once_cell",
]
[[package]]
name = "threadpool"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
dependencies = [
"num_cpus",
]
[[package]]
name = "tiff"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471"
dependencies = [
"flate2",
"jpeg-decoder",
"weezl",
]
[[package]]
name = "time"
version = "0.3.17"
@ -1330,6 +1634,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "weezl"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
[[package]]
name = "winapi"
version = "0.3.9"
@ -1416,6 +1726,7 @@ dependencies = [
"axum",
"axum-client-ip",
"bytes",
"image",
"lazy_static",
"rand",
"rusqlite",

View file

@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
tokio = { version = "1.23.0", features = ["full"] }
axum = { version = "0.6.4", features = ["headers"] }
axum = { version = "0.6.4", features = ["headers", "query"] }
axum-client-ip = "0.3.1"
tower-http = { version = "0.3.5", features = ["fs"] }
tower_governor = "0.0.4"
@ -20,3 +20,4 @@ rusqlite = { version = "0.28.0", features = ["bundled"] }
rand = "0.8.5"
time = "0.3.17"
lazy_static = "1.4.0"
image = "0.24.3"

View file

@ -30,15 +30,17 @@ If you want to run it in a docker container a premade dockerfile is here for you
There is also a docker-compose.yml file for your reference in the /deployments/docker folder.
The one thing about the docker container is you have to mount the volume
`-v [your directory]/xssbook.db:/data/xssbook.db`
to make the database persistant. Finally, before running the container run
There are two volumes you have to make for the container. First one for the database otherwise all data will be wiped upon container restart. You only should volume the database file so create the vollume with the directory below.
`touch [your directory]/xssbook.db`
since docker will create a folder there otherwise and it won't work.
`-v [your directory]/xssbook.db:/data/xssbook.db`
You have to create the database file beforehand because otherwise docker will create a folder there instead, and then the program will crash when it tries to load a folder as a database.
Finally, you have to make a volume to store custom user avatars and banners. Without this, this data too will be lost upon contaienr restart. To make the volume simply run this with your container.
`-v [another directory]:/data/public/image/custom`
**reverse proxy**

View file

@ -13,6 +13,8 @@ COPY --from=builder /usr/local/cargo/bin/xssbook /usr/local/bin/xssbook
RUN mkdir /data
WORKDIR /data
COPY ./public ./public
RUN mkdir ./public/image/custom
VOLUME ./public/image/custom
EXPOSE 8080
CMD ["/usr/local/bin/xssbook"]

View file

@ -10,3 +10,4 @@ services:
- 8080:8080
volumes:
- ${PWD}/xssbook.db:/data/xssbook.db
- ${PWD}/custom:/data/public/image/custom

View file

@ -368,4 +368,14 @@ form {
input:focus {
outline: none;
}
.changeavatar {
filter: invert(100%) !important;
background-color: #bbbbbb !important;
}
.changebanner {
filter: invert(100%) !important;
background-color: #bbbbbb !important;
}
}

File diff suppressed because one or more lines are too long

View file

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View file

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View file

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 8 KiB

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View file

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

View file

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View file

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View file

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View file

@ -1,6 +1,27 @@
const endpoint = '/api'
const fileRequest = async (url, file, method) => {
if (method === undefined) method = 'POST'
const response = await fetch(endpoint + url, {
method,
body: file,
headers: {}
});
if (response.status == 401) {
location.href = 'login'
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
const json = await response.json()
return { status: response.status, msg: json.msg, json }
} else {
const msg = await response.text();
return { status: response.status, msg }
}
}
const request = async (url, body, method) => {
if (method === undefined) method = 'POST'
const response = await fetch(endpoint + url, {
method,
@ -89,3 +110,11 @@ const adminusers = async () => {
const adminsessions = async () => {
return await request('/admin/sessions', {})
}
const updateavatar = async (file) => {
return await fileRequest('/users/avatar', file, 'PUT')
}
const updatebanner = async (file) => {
return await fileRequest('/users/banner', file, 'PUT')
}

View file

@ -33,7 +33,11 @@ function remove(id) {
}
function pfp(id) {
return `<img src="/img/${id % 25}.png">`
return `<img src="/image/avatar?user_id=${id}">`
}
function banner(id) {
return `<img src="/image/banner?user_id=${id}" onerror="this.remove()" >`
}
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',

View file

@ -16,24 +16,44 @@ function swap(value) {
}
}
function changeimage(fn) {
var input = document.createElement('input')
input.type = 'file'
input.accept= 'image/png'
input.onchange = async (e) => {
var file = e.target.files[0];
if (file.type !== 'image/png') {
return
}
let response = await fn(file);
alert(response.msg)
}
input.click();
}
function render() {
const html = `
<div id="top">
<div id="banner">
<div>
<div class="bg">
${banner(data.user.user_id)}
</div>
${ isself ? `<div class="changebanner" onclick="changeimage(updatebanner)"></div>` : '' }
</div>
<div id="info">
<div class="face">
${pfp(data.user.user_id)}
${ isself ? `<div class="changeavatar" onclick="changeimage(updateavatar)"></div>` : '' }
</div>
<div class="infodata">
<span class="bold ltext">${data.user.firstname + ' ' + data.user.lastname}</span>
<span class="gtext">Joined ${parseDate(new Date(data.user.date))}</span>
</div>
</div>
<div class="fullline" style="width: 80em; margin-bottom: 0;"></div>
<div class="fullline" style="width: 80em; margin-bottom: 0; z-index: 0;"></div>
<div class="profilebuttons">
<button id="profilepostbutton" class="${posts ? 'selected' : ''}" onclick="swap(true)">
Posts
@ -71,6 +91,7 @@ function render() {
`
append(about)
}
async function logout_button() {

54
src/api/image.rs Normal file
View file

@ -0,0 +1,54 @@
use axum::{extract::Query, response::Response, routing::get, Router, http::StatusCode};
use serde::Deserialize;
use crate::types::http::ResponseCode;
#[derive(Deserialize)]
struct AvatarRequest {
user_id: u64,
}
async fn avatar(params: Option<Query<AvatarRequest>>) -> Response {
let Some(params) = params else {
return ResponseCode::BadRequest.text("Missing query paramaters");
};
let custom = format!("/image/custom/avatar/{}.png", params.user_id);
let default = format!("/image/default/{}.png", params.user_id % 25);
let file = ResponseCode::Success.file(&custom).await;
if file.status() != StatusCode::OK {
return ResponseCode::Success.file(&default).await
}
file
}
#[derive(Deserialize)]
struct BannerRequest {
user_id: u64,
}
async fn banner(params: Option<Query<BannerRequest>>) -> Response {
let Some(params) = params else {
return ResponseCode::BadRequest.text("Missing query paramaters");
};
let custom = format!("/image/custom/banner/{}.png", params.user_id);
let file = ResponseCode::Success.file(&custom).await;
if file.status() != StatusCode::OK {
return ResponseCode::NotFound.text("User does not have a custom banner")
}
file
}
pub fn router() -> Router {
Router::new()
.route("/avatar", get(avatar))
.route("/banner", get(banner))
}

View file

@ -8,6 +8,7 @@ pub mod auth;
pub mod pages;
pub mod posts;
pub mod users;
pub mod image;
pub fn router() -> Router {
let governor_conf = Box::new(

View file

@ -1,9 +1,9 @@
use crate::types::{
extract::{AuthorizedUser, Check, CheckResult, Json},
extract::{AuthorizedUser, Check, CheckResult, Json, Png},
http::ResponseCode,
user::User,
};
use axum::{response::Response, routing::post, Router};
use axum::{response::Response, routing::{post, put}, Router};
use serde::Deserialize;
#[derive(Deserialize)]
@ -63,9 +63,33 @@ async fn load_self(AuthorizedUser(user): AuthorizedUser) -> Response {
ResponseCode::Success.json(&json)
}
async fn avatar(AuthorizedUser(user): AuthorizedUser, Png(img): Png) -> Response {
let path = format!("./public/image/custom/avatar/{}.png", user.user_id);
if img.save(path).is_err() {
return ResponseCode::InternalServerError.text("Failed to update avatar");
}
ResponseCode::Success.text("Successfully updated avatar")
}
async fn banner(AuthorizedUser(user): AuthorizedUser, Png(img): Png) -> Response {
let path = format!("./public/image/custom/banner/{}.png", user.user_id);
if img.save(path).is_err() {
return ResponseCode::InternalServerError.text("Failed to update banner");
}
ResponseCode::Success.text("Successfully updated banner")
}
pub fn router() -> Router {
Router::new()
.route("/load", post(load_batch))
.route("/self", post(load_self))
.route("/page", post(load_page))
.route("/avatar", put(avatar))
.route("/banner", put(banner))
}

View file

@ -3,10 +3,10 @@ use axum::{
http::{Request, StatusCode},
middleware::{self, Next},
response::Response,
RequestExt, Router,
RequestExt, Router, extract::DefaultBodyLimit,
};
use axum_client_ip::ClientIp;
use std::{net::SocketAddr, process::exit};
use std::{net::SocketAddr, process::exit, fs};
use tower_cookies::CookieManagerLayer;
use tracing::{error, info, metadata::LevelFilter};
use tracing_subscriber::{
@ -14,7 +14,7 @@ use tracing_subscriber::{
};
use types::http::ResponseCode;
use crate::api::pages;
use crate::api::{pages, image};
mod admin;
mod api;
@ -69,13 +69,19 @@ async fn main() {
exit(1)
};
fs::create_dir_all("./public/image/custom").expect("Coudn't make custom data directory");
fs::create_dir_all("./public/image/custom/avatar").expect("Coudn't make custom avatar directory");
fs::create_dir_all("./public/image/custom/banner").expect("Coudn't make custom banner directory");
let app = Router::new()
.fallback(not_found)
.layer(middleware::from_fn(log))
.layer(middleware::from_fn(serve))
.nest("/", pages::router())
.nest("/api", api::router())
.layer(CookieManagerLayer::new());
.nest("/image", image::router())
.layer(CookieManagerLayer::new())
.layer(DefaultBodyLimit::max(512_000));
let Ok(addr) = "[::]:8080".parse::<std::net::SocketAddr>() else {
error!("Failed to parse port binding");

View file

@ -1,4 +1,4 @@
use std::io::Read;
use std::io::{Read, Cursor};
use axum::{
async_trait,
@ -10,6 +10,7 @@ use axum::{
};
use axum_client_ip::ClientIp;
use bytes::Bytes;
use image::{io::Reader, ImageFormat, DynamicImage};
use serde::de::DeserializeOwned;
use tower_cookies::Cookies;
@ -99,6 +100,36 @@ where
}
}
pub struct Png(pub DynamicImage);
#[async_trait]
impl<S, B> FromRequest<S, B> for Png
where
B: HttpBody + Sync + Send + 'static,
B::Data: Send,
B::Error: Into<BoxError>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(req: Request<B>, state: &S) -> Result<Self> {
let bytes = match read_body(req, state).await {
Ok(body) => body,
Err(err) => return Err(err),
};
let mut reader = Reader::new(Cursor::new(bytes));
reader.set_format(ImageFormat::Png);
let Ok(img) = reader.decode() else {
return Err(ResponseCode::BadRequest.text("Failed to decode png image"))
};
Ok(Self(img))
}
}
pub struct Json<T>(pub T);
#[async_trait]
@ -150,7 +181,43 @@ pub trait Check {
}
}
pub async fn parse_body<S, B>(mut req: Request<B>, state: &S) -> Result<String>
async fn read_body<S, B>(mut req: Request<B>, state: &S) -> Result<Vec<u8>>
where
B: HttpBody + Sync + Send + 'static,
B::Data: Send,
B::Error: Into<BoxError>,
S: Send + Sync,
{
let Ok(ClientIp(ip)) = req.extract_parts::<ClientIp>().await else {
tracing::error!("Failed to read client ip");
return Err(ResponseCode::InternalServerError.text("Failed to read client ip"));
};
let method = req.method().clone();
let uri = req.uri().clone();
let path = req
.extensions()
.get::<RouterURI>()
.map_or("", |path| path.0);
let Ok(bytes) = Bytes::from_request(req, state).await else {
return Err(ResponseCode::BadRequest.text("Request can be at most 512kb"));
};
console::log(
ip,
method,
uri,
Some(path.to_string()),
None,
)
.await;
Ok(bytes.bytes().flatten().collect())
}
async fn parse_body<S, B>(mut req: Request<B>, state: &S) -> Result<String>
where
B: HttpBody + Sync + Send + 'static,
B::Data: Send,
@ -170,8 +237,7 @@ where
.map_or("", |path| path.0);
let Ok(bytes) = Bytes::from_request(req, state).await else {
tracing::error!("Failed to read request body");
return Err(ResponseCode::InternalServerError.text("Failed to read request body"));
return Err(ResponseCode::BadRequest.text("Request can be at most 512kb"));
};
let Ok(body) = String::from_utf8(bytes.bytes().flatten().collect()) else {