Авторизация через токен вк

Слияние schedule_parser с проектом

Перенос схемы запросов/ответов в файлы эндпоинтов

Переход с библиотеки jwt на jsonwebtokens
This commit is contained in:
2025-03-25 02:05:27 +04:00
parent 0316f58592
commit ab1cbd795e
22 changed files with 794 additions and 369 deletions

View File

@@ -24,7 +24,7 @@ jobs:
- name: Create .env.test - name: Create .env.test
run: touch .env.test run: touch .env.test
- name: Run tests - name: Run tests
run: cargo test -p schedule-parser-rusted -p schedule_parser run: cargo test
env: env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
JWT_SECRET: "test-secret-at-least-256-bits-used" JWT_SECRET: "test-secret-at-least-256-bits-used"

14
.idea/discord.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
<option name="applicationTheme" value="default" />
<option name="iconsTheme" value="default" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
<option name="customApplicationId" value="" />
</component>
</project>

View File

@@ -2,9 +2,9 @@
<module type="EMPTY_MODULE" version="4"> <module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/lib/schedule_parser/benches" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/lib/schedule_parser/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/lib/schedule_parser/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/benches" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

230
Cargo.lock generated
View File

@@ -29,7 +29,7 @@ dependencies = [
"actix-rt", "actix-rt",
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"base64 0.22.1", "base64",
"bitflags", "bitflags",
"brotli", "brotli",
"bytes", "bytes",
@@ -287,12 +287,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@@ -305,7 +299,7 @@ version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f" checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"blowfish", "blowfish",
"getrandom 0.3.2", "getrandom 0.3.2",
"subtle", "subtle",
@@ -327,6 +321,15 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "block-buffer"
version = "0.11.0-rc.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a229bfd78e4827c91b9b95784f69492c1b77c1ab75a45a8a037b139215086f94"
dependencies = [
"hybrid-array",
]
[[package]] [[package]]
name = "blowfish" name = "blowfish"
version = "0.9.1" version = "0.9.1"
@@ -477,7 +480,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [ dependencies = [
"crypto-common", "crypto-common 0.1.6",
"inout", "inout",
] ]
@@ -515,6 +518,12 @@ dependencies = [
"encoding_rs", "encoding_rs",
] ]
[[package]]
name = "const-oid"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cb3c4a0d3776f7535c32793be81d6d5fec0d48ac70955d9834e643aa249a52f"
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.16.2" version = "0.16.2"
@@ -637,6 +646,15 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "crypto-common"
version = "0.2.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "170d71b5b14dec99db7739f6fc7d6ec2db80b78c3acb77db48392ccc3d8a9ea0"
dependencies = [
"hybrid-array",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.20.10" version = "0.20.10"
@@ -679,6 +697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
"serde",
] ]
[[package]] [[package]]
@@ -765,9 +784,19 @@ version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer 0.10.4",
"crypto-common", "crypto-common 0.1.6",
"subtle", ]
[[package]]
name = "digest"
version = "0.11.0-pre.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c478574b20020306f98d61c8ca3322d762e1ff08117422ac6106438605ea516"
dependencies = [
"block-buffer 0.11.0-rc.4",
"const-oid",
"crypto-common 0.2.0-rc.2",
] ]
[[package]] [[package]]
@@ -961,8 +990,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -995,7 +1026,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http 0.2.12", "http 0.2.12",
"indexmap", "indexmap 2.8.0",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@@ -1014,7 +1045,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"http 1.3.1", "http 1.3.1",
"indexmap", "indexmap 2.8.0",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@@ -1031,6 +1062,12 @@ dependencies = [
"crunchy", "crunchy",
] ]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.2" version = "0.15.2"
@@ -1050,13 +1087,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
[[package]] [[package]]
name = "hmac" name = "hex"
version = "0.12.1" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "hostname" name = "hostname"
@@ -1125,6 +1159,15 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hybrid-array"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dab50e193aebe510fe0e40230145820e02f48dae0cf339ea4204e6e708ff7bd"
dependencies = [
"typenum",
]
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.6.0" version = "1.6.0"
@@ -1371,6 +1414,17 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.8.0" version = "2.8.0"
@@ -1378,7 +1432,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown 0.15.2",
"serde",
] ]
[[package]] [[package]]
@@ -1442,18 +1497,18 @@ dependencies = [
] ]
[[package]] [[package]]
name = "jwt" name = "jsonwebtoken"
version = "0.16.0" version = "9.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
dependencies = [ dependencies = [
"base64 0.13.1", "base64",
"crypto-common", "js-sys",
"digest", "pem",
"hmac", "ring",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "simple_asn1",
] ]
[[package]] [[package]]
@@ -1575,12 +1630,31 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -1694,6 +1768,16 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "pem"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
dependencies = [
"base64",
"serde",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@@ -1958,7 +2042,7 @@ version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@@ -2119,38 +2203,30 @@ dependencies = [
[[package]] [[package]]
name = "schedule-parser-rusted" name = "schedule-parser-rusted"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"actix-http", "actix-http",
"actix-web", "actix-web",
"bcrypt", "bcrypt",
"chrono",
"diesel",
"diesel-derive-enum",
"dotenvy",
"hmac",
"jwt",
"mime",
"objectid",
"reqwest",
"schedule_parser",
"serde",
"serde_json",
"sha2",
"tokio",
]
[[package]]
name = "schedule_parser"
version = "0.2.0"
dependencies = [
"calamine", "calamine",
"chrono", "chrono",
"criterion", "criterion",
"diesel",
"diesel-derive-enum",
"dotenvy",
"fuzzy-matcher", "fuzzy-matcher",
"jsonwebtoken",
"mime",
"objectid",
"rand 0.9.0",
"regex", "regex",
"reqwest",
"serde", "serde",
"serde_json",
"serde_repr", "serde_repr",
"serde_with",
"sha2",
"tokio",
] ]
[[package]] [[package]]
@@ -2237,6 +2313,36 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_with"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
dependencies = [
"base64",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.8.0",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time 0.3.40",
]
[[package]]
name = "serde_with_macros"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@@ -2245,18 +2351,18 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures",
"digest", "digest 0.10.7",
] ]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.8" version = "0.11.0-pre.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" checksum = "19b4241d1a56954dce82cecda5c8e9c794eef6f53abe5e5216bac0a0ea71ffa7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures",
"digest", "digest 0.11.0-pre.10",
] ]
[[package]] [[package]]
@@ -2280,6 +2386,18 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simple_asn1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror",
"time 0.3.40",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@@ -3142,7 +3260,7 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
"displaydoc", "displaydoc",
"flate2", "flate2",
"indexmap", "indexmap 2.8.0",
"memchr", "memchr",
"thiserror", "thiserror",
"zopfli", "zopfli",

View File

@@ -1,27 +1,35 @@
[workspace]
members = ["lib/schedule_parser"]
[package] [package]
name = "schedule-parser-rusted" name = "schedule-parser-rusted"
version = "0.3.0" version = "0.4.0"
edition = "2024" edition = "2024"
publish = false publish = false
[dependencies] [dependencies]
actix-http = "3.10.0"
actix-web = "4.10.2" actix-web = "4.10.2"
bcrypt = "0.17.0" bcrypt = "0.17.0"
chrono = "0.4.40" calamine = "0.26.1"
chrono = { version = "0.4.40", features = ["serde"] }
diesel = { version = "2.2.8", features = ["postgres"] } diesel = { version = "2.2.8", features = ["postgres"] }
diesel-derive-enum = { git = "https://github.com/Havunen/diesel-derive-enum.git", features = ["postgres"] } diesel-derive-enum = { git = "https://github.com/Havunen/diesel-derive-enum.git", features = ["postgres"] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
hmac = "0.12.1" fuzzy-matcher = "0.3.7"
jwt = "0.16.0" jsonwebtoken = { version = "9.3.1", features = ["use_pem"] }
mime = "0.3.17" mime = "0.3.17"
objectid = "0.2.0" objectid = "0.2.0"
regex = "1.11.1"
reqwest = "0.12.15" reqwest = "0.12.15"
schedule_parser = { path = "./lib/schedule_parser" }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
sha2 = "0.10.8" serde_with = "3.12.0"
serde_repr = "0.1.20"
sha2 = "0.11.0-pre.5"
tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
rand = "0.9.0"
[dev-dependencies]
actix-http = "3.10.0"
criterion = "0.5.1"
[[bench]]
name = "parse"
harness = false

View File

@@ -1,8 +1,9 @@
use criterion::{Criterion, criterion_group, criterion_main}; use criterion::{Criterion, criterion_group, criterion_main};
use schedule_parser::parse_xls;
use schedule_parser_rusted::parser::parse_xls;
pub fn bench_parse_xls(c: &mut Criterion) { pub fn bench_parse_xls(c: &mut Criterion) {
let buffer: Vec<u8> = include_bytes!("../../../schedule.xls").to_vec(); let buffer: Vec<u8> = include_bytes!("../schedule.xls").to_vec();
c.bench_function("parse_xls", |b| b.iter(|| parse_xls(&buffer))); c.bench_function("parse_xls", |b| b.iter(|| parse_xls(&buffer)));
} }

View File

@@ -1,23 +0,0 @@
[package]
name = "schedule_parser"
version = "0.2.0"
edition = "2024"
[lib]
name = "schedule_parser"
path = "src/lib/lib.rs"
[dependencies]
serde = { version = "1.0.219", features = ["derive"] }
serde_repr = "0.1.20"
chrono = { version = "0.4.40", features = ["serde"] }
calamine = "0.26.1"
regex = "1.11.1"
fuzzy-matcher = "0.3.7"
[dev-dependencies]
criterion = "0.5.1"
[[bench]]
name = "parse"
harness = false

View File

@@ -2,9 +2,9 @@ use crate::xls_downloader::basic_impl::BasicXlsDownloader;
use actix_web::web; use actix_web::web;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use diesel::{Connection, PgConnection}; use diesel::{Connection, PgConnection};
use schedule_parser::schema::ParseResult;
use std::env; use std::env;
use std::sync::{Mutex, MutexGuard}; use std::sync::{Mutex, MutexGuard};
use crate::parser::schema::ParseResult;
pub struct Schedule { pub struct Schedule {
pub etag: String, pub etag: String,

View File

@@ -2,13 +2,13 @@ pub mod users {
use crate::database::models::User; use crate::database::models::User;
use crate::database::schema::users::dsl::users; use crate::database::schema::users::dsl::users;
use crate::database::schema::users::dsl::*; use crate::database::schema::users::dsl::*;
use diesel::{insert_into, ExpressionMethods, QueryResult}; use diesel::{ExpressionMethods, QueryResult, insert_into};
use diesel::{PgConnection, SelectableHelper}; use diesel::{PgConnection, SelectableHelper};
use diesel::{QueryDsl, RunQueryDsl}; use diesel::{QueryDsl, RunQueryDsl};
use std::ops::DerefMut; use std::ops::DerefMut;
use std::sync::Mutex; use std::sync::Mutex;
pub fn get(connection: &Mutex<PgConnection>, _id: String) -> QueryResult<User> { pub fn get(connection: &Mutex<PgConnection>, _id: &String) -> QueryResult<User> {
let mut lock = connection.lock().unwrap(); let mut lock = connection.lock().unwrap();
let con = lock.deref_mut(); let con = lock.deref_mut();
@@ -20,7 +20,7 @@ pub mod users {
pub fn get_by_username( pub fn get_by_username(
connection: &Mutex<PgConnection>, connection: &Mutex<PgConnection>,
_username: String, _username: &String,
) -> QueryResult<User> { ) -> QueryResult<User> {
let mut lock = connection.lock().unwrap(); let mut lock = connection.lock().unwrap();
let con = lock.deref_mut(); let con = lock.deref_mut();
@@ -30,8 +30,21 @@ pub mod users {
.select(User::as_select()) .select(User::as_select())
.first(con) .first(con)
} }
pub fn get_by_vk_id(
connection: &Mutex<PgConnection>,
_vk_id: i32,
) -> QueryResult<User> {
let mut lock = connection.lock().unwrap();
let con = lock.deref_mut();
pub fn contains_by_username(connection: &Mutex<PgConnection>, _username: String) -> bool { users
.filter(vk_id.eq(_vk_id))
.select(User::as_select())
.first(con)
}
pub fn contains_by_username(connection: &Mutex<PgConnection>, _username: &String) -> bool {
let mut lock = connection.lock().unwrap(); let mut lock = connection.lock().unwrap();
let con = lock.deref_mut(); let con = lock.deref_mut();
@@ -45,7 +58,21 @@ pub mod users {
} }
} }
pub fn delete_by_username(connection: &Mutex<PgConnection>, _username: String) -> bool { pub fn contains_by_vk_id(connection: &Mutex<PgConnection>, _vk_id: i32) -> bool {
let mut lock = connection.lock().unwrap();
let con = lock.deref_mut();
match users
.filter(vk_id.eq(_vk_id))
.count()
.get_result::<i64>(con)
{
Ok(count) => count > 0,
Err(_) => false,
}
}
pub fn delete_by_username(connection: &Mutex<PgConnection>, _username: &String) -> bool {
let mut lock = connection.lock().unwrap(); let mut lock = connection.lock().unwrap();
let con = lock.deref_mut(); let con = lock.deref_mut();
@@ -61,11 +88,14 @@ pub mod users {
insert_into(users).values(user).execute(con) insert_into(users).values(user).execute(con)
} }
pub fn insert_or_ignore(connection: &Mutex<PgConnection>, user: &User) -> QueryResult<usize> { pub fn insert_or_ignore(connection: &Mutex<PgConnection>, user: &User) -> QueryResult<usize> {
let mut lock = connection.lock().unwrap(); let mut lock = connection.lock().unwrap();
let con = lock.deref_mut(); let con = lock.deref_mut();
insert_into(users).values(user).on_conflict_do_nothing().execute(con) insert_into(users)
.values(user)
.on_conflict_do_nothing()
.execute(con)
} }
} }

View File

@@ -23,4 +23,4 @@ pub struct User {
pub group: String, pub group: String,
pub role: UserRole, pub role: UserRole,
pub version: String, pub version: String,
} }

1
src/lib.rs Normal file
View File

@@ -0,0 +1 @@
pub mod parser;

View File

@@ -1,5 +1,6 @@
use crate::app_state::{AppState, app_state}; use crate::app_state::{AppState, app_state};
use crate::routes::auth::sign_in::sign_in; use crate::routes::auth::sign_in::{sign_in_default, sign_in_vk};
use crate::routes::auth::sign_up::{sign_up_default, sign_up_vk};
use actix_web::{App, HttpServer, web}; use actix_web::{App, HttpServer, web};
use dotenvy::dotenv; use dotenvy::dotenv;
@@ -7,19 +8,24 @@ mod app_state;
mod database; mod database;
mod routes; mod routes;
#[cfg(test)]
mod test_env; mod test_env;
mod utility; mod utility;
mod xls_downloader; mod xls_downloader;
mod parser;
#[actix_web::main] #[actix_web::main]
async fn main() { async fn main() {
dotenv().ok(); dotenv().ok();
HttpServer::new(move || { HttpServer::new(move || {
let schedule_scope = web::scope("/auth").service(sign_in); let auth_scope = web::scope("/auth")
let api_scope = web::scope("/api/v1").service(schedule_scope); .service(sign_in_default)
.service(sign_in_vk)
.service(sign_up_default)
.service(sign_up_vk);
let api_scope = web::scope("/api/v1").service(auth_scope);
App::new().app_data(move || app_state()).service(api_scope) App::new().app_data(move || app_state()).service(api_scope)
}) })

View File

@@ -1,6 +1,6 @@
use crate::LessonParseResult::{Lessons, Street}; use crate::parser::LessonParseResult::{Lessons, Street};
use crate::schema::LessonType::Break; use crate::parser::schema::LessonType::Break;
use crate::schema::{ use crate::parser::schema::{
Day, Lesson, LessonSubGroup, LessonTime, LessonType, ParseResult, ScheduleEntry, Day, Lesson, LessonSubGroup, LessonTime, LessonType, ParseResult, ScheduleEntry,
}; };
use calamine::{Reader, Xls, open_workbook_from_rs}; use calamine::{Reader, Xls, open_workbook_from_rs};
@@ -690,13 +690,18 @@ pub fn parse_xls(buffer: &Vec<u8>) -> ParseResult {
} }
#[cfg(test)] #[cfg(test)]
mod tests { pub mod tests {
use super::*; use super::*;
pub fn test_result() -> ParseResult {
let buffer: Vec<u8> = include_bytes!("../../schedule.xls").to_vec();
parse_xls(&buffer)
}
#[test] #[test]
fn read() { fn read() {
let buffer: Vec<u8> = include_bytes!("../../../../schedule.xls").to_vec(); let result = test_result();
let result = parse_xls(&buffer);
assert_ne!(result.groups.len(), 0); assert_ne!(result.groups.len(), 0);
assert_ne!(result.teachers.len(), 0); assert_ne!(result.teachers.len(), 0);

View File

@@ -1,3 +1,3 @@
mod schema;
pub mod sign_in; pub mod sign_in;
pub mod sign_up; pub mod sign_up;
mod shared;

View File

@@ -1,109 +0,0 @@
pub mod sign_in {
use crate::database::models::User;
use crate::routes::schema::shared::{ErrorToHttpCode, IResponse};
use crate::routes::schema::user;
use actix_web::http::StatusCode;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct Request {
pub username: String,
pub password: String,
}
pub type Response = IResponse<user::ResponseOk, ResponseErr>;
#[derive(Serialize)]
pub struct ResponseErr {
code: ErrorCode,
}
#[derive(Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
IncorrectCredentials,
InvalidVkAccessToken,
}
pub trait ResponseExt {
fn ok(user: &User) -> Self;
fn err(code: ErrorCode) -> Response;
}
impl ResponseExt for Response {
fn ok(user: &User) -> Self {
IResponse(Ok(user::ResponseOk::from_user(&user)))
}
fn err(code: ErrorCode) -> Response {
IResponse(Err(ResponseErr { code }))
}
}
impl ErrorToHttpCode for ResponseErr {
fn to_http_status_code(&self) -> StatusCode {
StatusCode::NOT_ACCEPTABLE
}
}
}
pub mod sign_up {
use crate::database::models::{User, UserRole};
use crate::routes::schema::shared::{ErrorToHttpCode, IResponse};
use crate::routes::schema::user;
use actix_web::http::StatusCode;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Request {
pub username: String,
pub password: String,
pub group: String,
pub role: UserRole,
pub version: String,
}
pub type Response = IResponse<user::ResponseOk, ResponseErr>;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResponseOk {
id: String,
access_token: String,
group: String,
}
#[derive(Serialize)]
pub struct ResponseErr {
code: ErrorCode,
}
#[derive(Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
DisallowedRole,
InvalidGroupName,
UsernameAlreadyExists,
}
pub trait ResponseExt {
fn ok(user: &User) -> Self;
fn err(code: ErrorCode) -> Self;
}
impl ResponseExt for Response {
fn ok(user: &User) -> Self {
IResponse(Ok(user::ResponseOk::from_user(&user)))
}
fn err(code: ErrorCode) -> Response {
Self(Err(ResponseErr { code }))
}
}
impl ErrorToHttpCode for ResponseErr {
fn to_http_status_code(&self) -> StatusCode {
StatusCode::NOT_ACCEPTABLE
}
}
}

96
src/routes/auth/shared.rs Normal file
View File

@@ -0,0 +1,96 @@
use crate::utility::jwt::DEFAULT_ALGORITHM;
use jsonwebtoken::errors::ErrorKind;
use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::env;
use std::sync::LazyLock;
#[derive(Deserialize, Serialize)]
struct TokenData {
iis: String,
sub: i32,
app: i32,
exp: i32,
iat: i32,
jti: i32,
}
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
iis: String,
jti: i32,
app: i32,
}
#[derive(Debug, PartialEq)]
pub enum Error {
JwtError(ErrorKind),
InvalidSignature,
InvalidToken,
Expired,
UnknownIssuer(String),
UnknownType(i32),
UnknownClientId(i32),
}
//noinspection SpellCheckingInspection
const VK_PUBLIC_KEY: &str = concat!(
"-----BEGIN PUBLIC KEY-----\n",
"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvsvJlhFX9Ju/pvCz1frB\n",
"DgJs592VjdwQuRAmnlJAItyHkoiDIOEocPzgcUBTbDf1plDcTyO2RCkUt0pz0WK6\n",
"6HNhpJyIfARjaWHeUlv4TpuHXAJJsBKklkU2gf1cjID+40sWWYjtq5dAkXnSJUVA\n",
"UR+sq0lJ7GmTdJtAr8hzESqGEcSP15PTs7VUdHZ1nkC2XgkuR8KmKAUb388ji1Q4\n",
"n02rJNOPQgd9r0ac4N2v/yTAFPXumO78N25bpcuWf5vcL9e8THk/U2zt7wf+aAWL\n",
"748e0pREqNluTBJNZfmhC79Xx6GHtwqHyyduiqfPmejmiujNM/rqnA4e30Tg86Yn\n",
"cNZ6vLJyF72Eva1wXchukH/aLispbY+EqNPxxn4zzCWaLKHG87gaCxpVv9Tm0jSD\n",
"2es22NjrUbtb+2pAGnXbyDp2eGUqw0RrTQFZqt/VcmmSCE45FlcZMT28otrwG1ZB\n",
"kZAb5Js3wLEch3ZfYL8sjhyNRPBmJBrAvzrd8qa3rdUjkC9sKyjGAaHu2MNmFl1Y\n",
"JFQ3J54tGpkGgJjD7Kz3w0K6OiPDlVCNQN5sqXm24fCw85Pbi8SJiaLTp/CImrs1\n",
"Z3nHW5q8hljA7OGmqfOP0nZS/5zW9GHPyepsI1rW6CympYLJ15WeNzePxYS5KEX9\n",
"EncmkSD9b45ge95hJeJZteUCAwEAAQ==\n",
"-----END PUBLIC KEY-----"
);
static VK_ID_CLIENT_ID: LazyLock<i32> = LazyLock::new(|| {
env::var("VK_ID_CLIENT_ID")
.expect("VK_ID_CLIENT_ID must be set")
.parse::<i32>()
.expect("VK_ID_CLIENT_ID must be i32")
});
pub fn parse_vk_id(token_str: &String) -> Result<i32, Error> {
let dkey = DecodingKey::from_rsa_pem(VK_PUBLIC_KEY.as_bytes()).unwrap();
match decode::<Claims>(&token_str, &dkey, &Validation::new(DEFAULT_ALGORITHM)) {
Ok(token_data) => {
let claims = token_data.claims;
if claims.iis != "VK" {
Err(Error::UnknownIssuer(claims.iis))
} else if claims.jti != 21 {
Err(Error::UnknownType(claims.jti))
} else if claims.app != *VK_ID_CLIENT_ID {
Err(Error::UnknownClientId(claims.app))
} else {
match claims.sub.parse::<i32>() {
Ok(sub) => Ok(sub),
Err(_) => Err(Error::InvalidToken),
}
}
}
Err(err) => Err(match err.into_kind() {
ErrorKind::InvalidToken => Error::InvalidToken,
ErrorKind::InvalidSignature => Error::InvalidSignature,
ErrorKind::InvalidAlgorithmName => Error::InvalidToken,
ErrorKind::MissingRequiredClaim(_) => Error::InvalidToken,
ErrorKind::ExpiredSignature => Error::Expired,
ErrorKind::InvalidAlgorithm => Error::InvalidToken,
ErrorKind::MissingAlgorithm => Error::InvalidToken,
ErrorKind::Base64(_) => Error::InvalidToken,
ErrorKind::Json(_) => Error::InvalidToken,
ErrorKind::Utf8(_) => Error::InvalidToken,
kind => Error::JwtError(kind),
}),
}
}

View File

@@ -1,47 +1,138 @@
use self::schema::*;
use crate::database::driver; use crate::database::driver;
use crate::database::models::User; use crate::database::models::User;
use crate::routes::auth::schema; use crate::routes::auth::shared::parse_vk_id;
use crate::{AppState, utility}; use crate::routes::auth::sign_in::schema::ErrorCode;
use crate::routes::auth::sign_in::schema::SignInData::{Default, Vk};
use crate::{utility, AppState};
use actix_web::{post, web}; use actix_web::{post, web};
use diesel::SaveChangesDsl; use diesel::SaveChangesDsl;
use std::ops::DerefMut; use std::ops::DerefMut;
use web::Json; use web::Json;
#[post("/sign-in")] async fn sign_in(data: SignInData, app_state: &web::Data<AppState>) -> Response {
pub async fn sign_in( let user = match &data {
data: Json<schema::sign_in::Request>, Default(data) => driver::users::get_by_username(&app_state.database, &data.username),
app_state: web::Data<AppState>, Vk(id) => driver::users::get_by_vk_id(&app_state.database, *id),
) -> schema::sign_in::Response { };
use schema::sign_in::*;
match driver::users::get_by_username(&app_state.database, data.username.clone()) { match user {
Ok(mut user) => match bcrypt::verify(&data.password, &user.password) { Ok(mut user) => {
Ok(true) => { if let Default(data) = data {
let mut lock = app_state.connection(); match bcrypt::verify(&data.password, &user.password) {
let conn = lock.deref_mut(); Ok(result) => {
if !result {
user.access_token = utility::jwt::encode(&user.id); return Response::err(ErrorCode::IncorrectCredentials);
}
user.save_changes::<User>(conn) }
.expect("Failed to update user"); Err(_) => {
return Response::err(ErrorCode::IncorrectCredentials);
Response::ok(&user) }
}
} }
Ok(false) | Err(_) => Response::err(ErrorCode::IncorrectCredentials),
}, let mut lock = app_state.connection();
let conn = lock.deref_mut();
user.access_token = utility::jwt::encode(&user.id);
user.save_changes::<User>(conn)
.expect("Failed to update user");
Response::ok(&user)
}
Err(_) => Response::err(ErrorCode::IncorrectCredentials), Err(_) => Response::err(ErrorCode::IncorrectCredentials),
} }
} }
#[post("/sign-in")]
pub async fn sign_in_default(data: Json<Request>, app_state: web::Data<AppState>) -> Response {
sign_in(Default(data.into_inner()), &app_state).await
}
#[post("/sign-in-vk")]
pub async fn sign_in_vk(data_json: Json<vk::Request>, app_state: web::Data<AppState>) -> Response {
let data = data_json.into_inner();
match parse_vk_id(&data.access_token) {
Ok(id) => sign_in(Vk(id), &app_state).await,
Err(_) => Response::err(ErrorCode::InvalidVkAccessToken),
}
}
mod schema {
use crate::database::models::User;
use crate::routes::schema::{user, ErrorToHttpCode, IResponse};
use actix_web::http::StatusCode;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct Request {
pub username: String,
pub password: String,
}
pub mod vk {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Request {
pub access_token: String,
}
}
pub type Response = IResponse<user::ResponseOk, ResponseErr>;
#[derive(Serialize)]
pub struct ResponseErr {
code: ErrorCode,
}
#[derive(Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
IncorrectCredentials,
InvalidVkAccessToken,
}
pub trait ResponseExt {
fn ok(user: &User) -> Self;
fn err(code: ErrorCode) -> Response;
}
impl ResponseExt for Response {
fn ok(user: &User) -> Self {
IResponse(Ok(user::ResponseOk::from_user(&user)))
}
fn err(code: ErrorCode) -> Response {
IResponse(Err(ResponseErr { code }))
}
}
impl ErrorToHttpCode for ResponseErr {
fn to_http_status_code(&self) -> StatusCode {
StatusCode::NOT_ACCEPTABLE
}
}
/// Internal
pub enum SignInData {
Default(Request),
Vk(i32),
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::app_state::app_state; use super::schema::*;
use crate::database::driver; use crate::database::driver;
use crate::database::models::{User, UserRole}; use crate::database::models::{User, UserRole};
use crate::routes::auth::schema; use crate::routes::auth::sign_in::sign_in_default;
use crate::routes::auth::sign_in::sign_in; use crate::test_env::tests::{static_app_state, test_app, test_app_state, test_env};
use crate::test_env::tests::{static_app_state, test_app, test_env};
use crate::utility; use crate::utility;
use actix_http::StatusCode; use actix_http::StatusCode;
use actix_web::dev::ServiceResponse; use actix_web::dev::ServiceResponse;
@@ -50,8 +141,8 @@ mod tests {
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::fmt::Write; use std::fmt::Write;
async fn sign_in_client(data: schema::sign_in::Request) -> ServiceResponse { async fn sign_in_client(data: Request) -> ServiceResponse {
let app = test_app(app_state(), sign_in).await; let app = test_app(test_app_state(), sign_in_default).await;
let req = test::TestRequest::with_uri("/sign-in") let req = test::TestRequest::with_uri("/sign-in")
.method(Method::POST) .method(Method::POST)
@@ -100,7 +191,7 @@ mod tests {
async fn sign_in_ok() { async fn sign_in_ok() {
prepare("test::sign_in_ok".to_string()); prepare("test::sign_in_ok".to_string());
let resp = sign_in_client(schema::sign_in::Request { let resp = sign_in_client(Request {
username: "test::sign_in_ok".to_string(), username: "test::sign_in_ok".to_string(),
password: "example".to_string(), password: "example".to_string(),
}) })
@@ -113,7 +204,7 @@ mod tests {
async fn sign_in_err() { async fn sign_in_err() {
prepare("test::sign_in_err".to_string()); prepare("test::sign_in_err".to_string());
let invalid_username = sign_in_client(schema::sign_in::Request { let invalid_username = sign_in_client(Request {
username: "test::sign_in_err::username".to_string(), username: "test::sign_in_err::username".to_string(),
password: "example".to_string(), password: "example".to_string(),
}) })
@@ -121,7 +212,7 @@ mod tests {
assert_eq!(invalid_username.status(), StatusCode::NOT_ACCEPTABLE); assert_eq!(invalid_username.status(), StatusCode::NOT_ACCEPTABLE);
let invalid_password = sign_in_client(schema::sign_in::Request { let invalid_password = sign_in_client(Request {
username: "test::sign_in_err".to_string(), username: "test::sign_in_err".to_string(),
password: "bad_password".to_string(), password: "bad_password".to_string(),
}) })

View File

@@ -1,22 +1,19 @@
use self::schema::*;
use crate::AppState;
use crate::database::driver; use crate::database::driver;
use crate::database::models::{User, UserRole}; use crate::database::models::UserRole;
use crate::routes::auth::schema; use crate::routes::auth::shared::{Error, parse_vk_id};
use crate::{utility, AppState};
use actix_web::{post, web}; use actix_web::{post, web};
use objectid::ObjectId; use rand::{Rng, rng};
use web::Json; use web::Json;
#[post("/sign-up")] async fn sign_up(data: SignUpData, app_state: &web::Data<AppState>) -> Response {
pub async fn sign_up( // If user selected forbidden role.
data: Json<schema::sign_up::Request>,
app_state: web::Data<AppState>,
) -> schema::sign_up::Response {
use schema::sign_up::*;
if data.role == UserRole::Admin { if data.role == UserRole::Admin {
return Response::err(ErrorCode::DisallowedRole); return Response::err(ErrorCode::DisallowedRole);
} }
// If specified group doesn't exist in schedule.
let schedule_opt = app_state.schedule.lock().unwrap(); let schedule_opt = app_state.schedule.lock().unwrap();
if let Some(schedule) = &*schedule_opt { if let Some(schedule) = &*schedule_opt {
@@ -25,37 +22,193 @@ pub async fn sign_up(
} }
} }
if driver::users::contains_by_username(&app_state.database, data.username.clone()) { // If user with specified username already exists.
if driver::users::contains_by_username(&app_state.database, &data.username) {
return Response::err(ErrorCode::UsernameAlreadyExists); return Response::err(ErrorCode::UsernameAlreadyExists);
} }
let id = ObjectId::new().unwrap().to_string(); // If user with specified VKID already exists.
let access_token = utility::jwt::encode(&id); if let Some(id) = data.vk_id {
if driver::users::contains_by_vk_id(&app_state.database, id) {
let user = User { return Response::err(ErrorCode::VkAlreadyExists);
id, }
username: data.username.clone(), }
password: bcrypt::hash(data.password.as_str(), bcrypt::DEFAULT_COST).unwrap(),
vk_id: None,
access_token,
group: data.group.clone(),
role: data.role.clone(),
version: data.version.clone(),
};
let user = data.to_user();
driver::users::insert(&app_state.database, &user).unwrap(); driver::users::insert(&app_state.database, &user).unwrap();
Response::ok(&user) Response::ok(&user)
} }
#[post("/sign-up")]
pub async fn sign_up_default(data_json: Json<Request>, app_state: web::Data<AppState>) -> Response {
let data = data_json.into_inner();
sign_up(
SignUpData {
username: data.username,
password: data.password,
vk_id: None,
group: data.group,
role: data.role,
version: data.version,
},
&app_state,
)
.await
}
#[post("/sign-up-vk")]
pub async fn sign_up_vk(data_json: Json<vk::Request>, app_state: web::Data<AppState>) -> Response {
let data = data_json.into_inner();
match parse_vk_id(&data.access_token) {
Ok(id) => {
sign_up(
SignUpData {
username: data.username,
password: rng()
.sample_iter(&rand::distr::Alphanumeric)
.take(16)
.map(char::from)
.collect(),
vk_id: Some(id),
group: data.group,
role: data.role,
version: data.version,
},
&app_state,
)
.await
}
Err(err) => {
if err != Error::Expired {
eprintln!("Failed to parse vk id token!");
eprintln!("{:?}", err);
}
Response::err(ErrorCode::InvalidVkAccessToken)
}
}
}
mod schema {
use crate::database::models::{User, UserRole};
use crate::routes::schema::{ErrorToHttpCode, IResponse, user};
use crate::utility;
use actix_web::http::StatusCode;
use objectid::ObjectId;
use serde::{Deserialize, Serialize};
/// WEB
#[derive(Serialize, Deserialize)]
pub struct Request {
pub username: String,
pub password: String,
pub group: String,
pub role: UserRole,
pub version: String,
}
pub mod vk {
use crate::database::models::UserRole;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Request {
pub access_token: String,
pub username: String,
pub group: String,
pub role: UserRole,
pub version: String,
}
}
pub type Response = IResponse<user::ResponseOk, ResponseErr>;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResponseOk {
id: String,
access_token: String,
group: String,
}
#[derive(Serialize)]
pub struct ResponseErr {
code: ErrorCode,
}
#[derive(Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
DisallowedRole,
InvalidGroupName,
UsernameAlreadyExists,
InvalidVkAccessToken,
VkAlreadyExists,
}
pub trait ResponseExt {
fn ok(user: &User) -> Self;
fn err(code: ErrorCode) -> Self;
}
impl ResponseExt for Response {
fn ok(user: &User) -> Self {
IResponse(Ok(user::ResponseOk::from_user(&user)))
}
fn err(code: ErrorCode) -> Response {
Self(Err(ResponseErr { code }))
}
}
impl ErrorToHttpCode for ResponseErr {
fn to_http_status_code(&self) -> StatusCode {
StatusCode::NOT_ACCEPTABLE
}
}
/// Internal
pub struct SignUpData {
pub username: String,
pub password: String,
pub vk_id: Option<i32>,
pub group: String,
pub role: UserRole,
pub version: String,
}
impl SignUpData {
pub fn to_user(self) -> User {
let id = ObjectId::new().unwrap().to_string();
let access_token = utility::jwt::encode(&id);
User {
id,
username: self.username,
password: bcrypt::hash(self.password, bcrypt::DEFAULT_COST).unwrap(),
vk_id: self.vk_id,
access_token,
group: self.group,
role: self.role,
version: self.version,
}
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::app_state::app_state;
use crate::database::driver; use crate::database::driver;
use crate::database::models::UserRole; use crate::database::models::UserRole;
use crate::routes::auth::schema; use crate::routes::auth::sign_up::schema::Request;
use crate::routes::auth::sign_up::sign_up; use crate::routes::auth::sign_up::sign_up_default;
use crate::test_env::tests::{static_app_state, test_app, test_env}; use crate::test_env::tests::{static_app_state, test_app, test_app_state, test_env};
use actix_http::StatusCode; use actix_http::StatusCode;
use actix_web::dev::ServiceResponse; use actix_web::dev::ServiceResponse;
use actix_web::http::Method; use actix_web::http::Method;
@@ -68,11 +221,11 @@ mod tests {
} }
async fn sign_up_client(data: SignUpPartial) -> ServiceResponse { async fn sign_up_client(data: SignUpPartial) -> ServiceResponse {
let app = test_app(app_state(), sign_up).await; let app = test_app(test_app_state(), sign_up_default).await;
let req = test::TestRequest::with_uri("/sign-up") let req = test::TestRequest::with_uri("/sign-up")
.method(Method::POST) .method(Method::POST)
.set_json(schema::sign_up::Request { .set_json(Request {
username: data.username.clone(), username: data.username.clone(),
password: "example".to_string(), password: "example".to_string(),
group: data.group.clone(), group: data.group.clone(),
@@ -91,7 +244,7 @@ mod tests {
test_env(); test_env();
let app_state = static_app_state(); let app_state = static_app_state();
driver::users::delete_by_username(&app_state.database, "test::sign_up_valid".to_string()); driver::users::delete_by_username(&app_state.database, &"test::sign_up_valid".to_string());
// test // test
@@ -114,7 +267,7 @@ mod tests {
let app_state = static_app_state(); let app_state = static_app_state();
driver::users::delete_by_username( driver::users::delete_by_username(
&app_state.database, &app_state.database,
"test::sign_up_multiple".to_string(), &"test::sign_up_multiple".to_string(),
); );
let create = sign_up_client(SignUpPartial { let create = sign_up_client(SignUpPartial {
@@ -150,4 +303,19 @@ mod tests {
assert_eq!(resp.status(), StatusCode::NOT_ACCEPTABLE); assert_eq!(resp.status(), StatusCode::NOT_ACCEPTABLE);
} }
#[actix_web::test]
async fn sign_up_invalid_group() {
test_env();
// test
let resp = sign_up_client(SignUpPartial {
username: "test::sign_up_invalid_group".to_string(),
group: "invalid_group".to_string(),
role: UserRole::Student,
})
.await;
assert_eq!(resp.status(), StatusCode::NOT_ACCEPTABLE);
}
} }

View File

@@ -1,39 +1,37 @@
pub mod shared { use actix_web::body::EitherBody;
use actix_web::body::EitherBody; use actix_web::error::JsonPayloadError;
use actix_web::error::JsonPayloadError; use actix_web::http::StatusCode;
use actix_web::http::StatusCode; use actix_web::{HttpRequest, HttpResponse, Responder};
use actix_web::{HttpRequest, HttpResponse, Responder}; use serde::Serialize;
use serde::Serialize;
pub struct IResponse<T: Serialize, E: Serialize>(pub Result<T, E>); pub struct IResponse<T: Serialize, E: Serialize>(pub Result<T, E>);
pub trait ErrorToHttpCode { pub trait ErrorToHttpCode {
fn to_http_status_code(&self) -> StatusCode; fn to_http_status_code(&self) -> StatusCode;
} }
impl<T: Serialize, E: Serialize + ErrorToHttpCode> Responder for IResponse<T, E> { impl<T: Serialize, E: Serialize + ErrorToHttpCode> Responder for IResponse<T, E> {
type Body = EitherBody<String>; type Body = EitherBody<String>;
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> { fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
match serde_json::to_string(&self.0) { match serde_json::to_string(&self.0) {
Ok(body) => { Ok(body) => {
let code = match &self.0 { let code = match &self.0 {
Ok(_) => StatusCode::OK, Ok(_) => StatusCode::OK,
Err(e) => e.to_http_status_code(), Err(e) => e.to_http_status_code(),
}; };
match HttpResponse::build(code) match HttpResponse::build(code)
.content_type(mime::APPLICATION_JSON) .content_type(mime::APPLICATION_JSON)
.message_body(body) .message_body(body)
{ {
Ok(res) => res.map_into_left_body(), Ok(res) => res.map_into_left_body(),
Err(err) => HttpResponse::from_error(err).map_into_right_body(), Err(err) => HttpResponse::from_error(err).map_into_right_body(),
}
} }
}
Err(err) => { Err(err) => {
HttpResponse::from_error(JsonPayloadError::Serialize(err)).map_into_right_body() HttpResponse::from_error(JsonPayloadError::Serialize(err)).map_into_right_body()
}
} }
} }
} }

View File

@@ -1,9 +1,10 @@
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use crate::app_state::{AppState, app_state}; use crate::app_state::{app_state, AppState, Schedule};
use actix_web::dev::{HttpServiceFactory, Service, ServiceResponse}; use actix_web::dev::{HttpServiceFactory, Service, ServiceResponse};
use actix_web::{App, test, web}; use actix_web::{test, web, App};
use std::sync::LazyLock; use std::sync::LazyLock;
use crate::parser::tests::test_result;
pub fn test_env() { pub fn test_env() {
dotenvy::from_path(".env.test").expect("Failed to load test environment file"); dotenvy::from_path(".env.test").expect("Failed to load test environment file");
@@ -19,8 +20,22 @@ pub(crate) mod tests {
test::init_service(App::new().app_data(app_state).service(factory)).await test::init_service(App::new().app_data(app_state).service(factory)).await
} }
pub fn test_app_state() -> web::Data<AppState> {
let state = app_state();
let mut schedule_lock = state.schedule.lock().unwrap();
*schedule_lock = Some(Schedule {
etag: "".to_string(),
updated_at: Default::default(),
parsed_at: Default::default(),
data: test_result(),
});
state.clone()
}
pub fn static_app_state() -> web::Data<AppState> { pub fn static_app_state() -> web::Data<AppState> {
static STATE: LazyLock<web::Data<AppState>> = LazyLock::new(|| app_state()); static STATE: LazyLock<web::Data<AppState>> = LazyLock::new(|| test_app_state());
STATE.clone() STATE.clone()
} }

View File

@@ -1,88 +1,91 @@
use chrono::DateTime;
use chrono::Duration; use chrono::Duration;
use chrono::TimeZone;
use chrono::Utc; use chrono::Utc;
use hmac::{Hmac, Mac}; use jsonwebtoken::errors::ErrorKind;
use jwt::{SignWithKey, Token, VerifyWithKey}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode};
use sha2::Sha256; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use serde_with::DisplayFromStr;
use serde_with::serde_as;
use std::env; use std::env;
use std::mem::discriminant; use std::mem::discriminant;
use std::sync::LazyLock; use std::sync::LazyLock;
static JWT_SECRET: LazyLock<Hmac<Sha256>> = LazyLock::new(|| { static DECODING_KEY: LazyLock<DecodingKey> = LazyLock::new(|| {
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set"); let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
Hmac::new_from_slice(secret.as_bytes()).expect("Hmac::new_from_slice failed") DecodingKey::from_secret(secret.as_bytes())
});
static ENCODING_KEY: LazyLock<EncodingKey> = LazyLock::new(|| {
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
EncodingKey::from_secret(secret.as_bytes())
}); });
#[derive(Debug)] #[derive(Debug)]
pub enum VerifyError { pub enum Error {
JwtError(jwt::Error),
InvalidSignature, InvalidSignature,
InvalidToken, InvalidToken(ErrorKind),
Expired, Expired,
} }
impl PartialEq for VerifyError { impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
discriminant(self) == discriminant(other) discriminant(self) == discriminant(other)
} }
} }
pub fn verify_and_decode(token: &String) -> Result<String, VerifyError> { #[serde_as]
let jwt = &*JWT_SECRET; #[derive(Debug, Serialize, Deserialize)]
struct Claims {
id: String,
#[serde_as(as = "DisplayFromStr")]
iat: u64,
#[serde_as(as = "DisplayFromStr")]
exp: u64,
}
let result: Result<BTreeMap<String, String>, jwt::Error> = token.verify_with_key(jwt); pub(crate) const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256;
pub fn verify_and_decode(token: &String) -> Result<String, Error> {
let mut validation = Validation::new(DEFAULT_ALGORITHM);
validation.required_spec_claims.remove("exp");
validation.validate_exp = false;
let result = decode::<Claims>(&token, &*DECODING_KEY, &validation);
match result { match result {
Ok(claims) => { Ok(token_data) => {
let exp = claims.get("exp").unwrap(); if token_data.claims.exp < Utc::now().timestamp().unsigned_abs() {
let exp_date = DateTime::from_timestamp(exp.parse::<i64>().unwrap(), 0) Err(Error::Expired)
.expect("Failed to parse expiration time"); } else {
Ok(token_data.claims.id)
if Utc::now() > exp_date {
return Err(VerifyError::Expired);
} }
Ok(claims.get("id").cloned().unwrap())
} }
Err(err) => Err(match err { Err(err) => Err(match err.into_kind() {
jwt::Error::InvalidSignature | jwt::Error::RustCryptoMac(_) => { ErrorKind::InvalidSignature => Error::InvalidSignature,
VerifyError::InvalidSignature ErrorKind::ExpiredSignature => Error::Expired,
} kind => Error::InvalidToken(kind),
jwt::Error::Format | jwt::Error::Base64(_) | jwt::Error::NoClaimsComponent => {
VerifyError::InvalidToken
}
_ => VerifyError::JwtError(err),
}), }),
} }
} }
pub fn encode(id: &String) -> String { pub fn encode(id: &String) -> String {
let header = jwt::Header { let header = Header {
type_: Some(jwt::header::HeaderType::JsonWebToken), typ: Some(String::from("JWT")),
..Default::default() ..Default::default()
}; };
let mut claims = BTreeMap::new();
let iat = Utc::now(); let iat = Utc::now();
let exp = iat + Duration::days(365 * 4); let exp = iat + Duration::days(365 * 4);
let iat_str = iat.timestamp().to_string(); let claims = Claims {
let exp_str = exp.timestamp().to_string(); id: id.clone(),
iat: iat.timestamp().unsigned_abs(),
exp: exp.timestamp().unsigned_abs(),
};
claims.insert("id", id.as_str()); jsonwebtoken::encode(&header, &claims, &*ENCODING_KEY).unwrap()
claims.insert("iat", iat_str.as_str());
claims.insert("exp", exp_str.as_str());
Token::new(header, claims)
.sign_with_key(&*JWT_SECRET)
.unwrap()
.as_str()
.to_string()
} }
#[cfg(test)] #[cfg(test)]
@@ -105,7 +108,10 @@ mod tests {
let result = verify_and_decode(&token); let result = verify_and_decode(&token);
assert!(result.is_err()); assert!(result.is_err());
assert_eq!(result.err().unwrap(), VerifyError::InvalidToken); assert_eq!(
result.err().unwrap(),
Error::InvalidToken(ErrorKind::InvalidToken)
);
} }
#[test] #[test]
@@ -116,7 +122,7 @@ mod tests {
let result = verify_and_decode(&token); let result = verify_and_decode(&token);
assert!(result.is_err()); assert!(result.is_err());
assert_eq!(result.err().unwrap(), VerifyError::InvalidSignature); assert_eq!(result.err().unwrap(), Error::InvalidSignature);
} }
#[test] #[test]
@@ -127,7 +133,7 @@ mod tests {
let result = verify_and_decode(&token); let result = verify_and_decode(&token);
assert!(result.is_err()); assert!(result.is_err());
assert_eq!(result.err().unwrap(), VerifyError::Expired); assert_eq!(result.err().unwrap(), Error::Expired);
} }
#[test] #[test]