From 70d83c284a073a2656d7034013ab223fa71144ff Mon Sep 17 00:00:00 2001 From: filipriec Date: Mon, 24 Mar 2025 21:46:04 +0100 Subject: [PATCH] broken only push user data --- Cargo.lock | 120 +++++++- common/build.rs | 1 + common/proto/auth.proto | 23 ++ common/src/lib.rs | 3 + common/src/proto/descriptor.bin | Bin 18994 -> 20131 bytes common/src/proto/multieko2.auth.rs | 318 ++++++++++++++++++++++ server/Cargo.toml | 5 +- server/migrations/20250324192805_auth.sql | 38 +++ server/src/auth/handlers.rs | 5 + server/src/auth/handlers/register.rs | 61 +++++ server/src/auth/mod.rs | 5 + server/src/auth/models.rs | 34 +++ server/src/lib.rs | 1 + 13 files changed, 608 insertions(+), 6 deletions(-) create mode 100644 common/proto/auth.proto create mode 100644 common/src/proto/multieko2.auth.rs create mode 100644 server/migrations/20250324192805_auth.sql create mode 100644 server/src/auth/handlers.rs create mode 100644 server/src/auth/handlers/register.rs create mode 100644 server/src/auth/mod.rs create mode 100644 server/src/auth/models.rs diff --git a/Cargo.lock b/Cargo.lock index 78f8bef..576f1e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -274,6 +274,19 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bcrypt" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f" +dependencies = [ + "base64", + "blowfish", + "getrandom 0.3.1", + "subtle", + "zeroize", +] + [[package]] name = "bigdecimal" version = "0.4.7" @@ -323,6 +336,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -386,6 +409,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "client" version = "0.1.0" @@ -782,7 +815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1452,6 +1485,15 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instability" version = "0.3.7" @@ -2030,6 +2072,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -2356,7 +2420,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2369,7 +2433,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.2", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2485,6 +2549,7 @@ dependencies = [ name = "server" version = "0.1.0" dependencies = [ + "bcrypt", "chrono", "common", "dashmap", @@ -2504,6 +2569,8 @@ dependencies = [ "tonic", "tonic-reflection", "tracing", + "uuid", + "validator", ] [[package]] @@ -2678,6 +2745,7 @@ dependencies = [ "tokio-stream", "tracing", "url", + "uuid", ] [[package]] @@ -2760,6 +2828,7 @@ dependencies = [ "thiserror 2.0.12", "time", "tracing", + "uuid", "whoami", ] @@ -2799,6 +2868,7 @@ dependencies = [ "thiserror 2.0.12", "time", "tracing", + "uuid", "whoami", ] @@ -2825,6 +2895,7 @@ dependencies = [ "time", "tracing", "url", + "uuid", ] [[package]] @@ -3029,7 +3100,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 1.0.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3473,6 +3544,45 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.1", +] + +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -3623,7 +3733,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/common/build.rs b/common/build.rs index e2d4acb..5383226 100644 --- a/common/build.rs +++ b/common/build.rs @@ -8,6 +8,7 @@ fn main() -> Result<(), Box> { &[ "proto/common.proto", "proto/adresar.proto", + "proto/auth.proto", "proto/uctovnictvo.proto", "proto/table_structure.proto", "proto/table_definition.proto", diff --git a/common/proto/auth.proto b/common/proto/auth.proto new file mode 100644 index 0000000..7a9585b --- /dev/null +++ b/common/proto/auth.proto @@ -0,0 +1,23 @@ +// proto/auth.proto +syntax = "proto3"; +package multieko2.auth; + +import "common.proto"; + +service AuthService { + rpc Register(RegisterRequest) returns (AuthResponse); +} + +message RegisterRequest { + string username = 1; + string email = 2; + string password = 3; + string password_confirmation = 4; +} + +message AuthResponse { + string id = 1; // UUID in string format + string username = 2; // Registered username + string email = 3; // Registered email (if provided) + string role = 4; // Default role: 'accountant' +} diff --git a/common/src/lib.rs b/common/src/lib.rs index f353400..786771e 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -4,6 +4,9 @@ pub mod proto { pub mod adresar { include!("proto/multieko2.adresar.rs"); } + pub mod auth { + include!("proto/multieko2.auth.rs"); + } pub mod common { include!("proto/multieko2.common.rs"); } diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 611ac520acd5dcfb0f3fc0ef535dd8a1ecdba44c..17d38ede51a9e12ac64d34f147a435c411c89601 100644 GIT binary patch delta 1093 zcmZuw!EVz)5cS%d)Ezr*v$oriR$^s}+6oawK|*jt^isrypi113m28SdV;9>gCzOw9 zublV+1b04$4?yA%m|1(R7IE9>nfKnz^X~U6_Vo+<_BmSqb^($_^?r0-NI=wMfZT_FBYVT$$0ldi1RO6%1hi}0n-8ZVZNGD1hgKM@Iw?{XV8a0*N1T)sjpFk3jbwFRqSJC<;`wFv-r z*Al#IxtD*oZ!MpK51p=MTTD1B?uJ3gTj@)K97Ok?5S1eptaXNGR6`E9Ti^lbs307V z4~RmbLkJAQM$ZcjLOE} zueGz0)@$r60&Dzkz!S_H50r~dDGT3RNoC@7q@1#wcsM& zk|!s}M>5T1Srut^DrY&}KDg{Z++Oyy9rmK4*yuy7nJ5$RK^#eQG1OF==TJ|?Y>Fdj z=0il3W-mmj7T{j@wHn3|@+g+q)${|B`^$7jm*XOxs_B7Iro_TlnGy?|pqWh}g&6dK zRz?u { + inner: tonic::client::Grpc, + } + impl AuthServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl AuthServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> AuthServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + AuthServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn register( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/multieko2.auth.AuthService/Register", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("multieko2.auth.AuthService", "Register")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod auth_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with AuthServiceServer. + #[async_trait] + pub trait AuthService: std::marker::Send + std::marker::Sync + 'static { + async fn register( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + } + #[derive(Debug)] + pub struct AuthServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl AuthServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for AuthServiceServer + where + T: AuthService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/multieko2.auth.AuthService/Register" => { + #[allow(non_camel_case_types)] + struct RegisterSvc(pub Arc); + impl< + T: AuthService, + > tonic::server::UnaryService + for RegisterSvc { + type Response = super::AuthResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::register(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = RegisterSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new(empty_body()); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for AuthServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "multieko2.auth.AuthService"; + impl tonic::server::NamedService for AuthServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/server/Cargo.toml b/server/Cargo.toml index ae2f57c..4b68978 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -12,7 +12,7 @@ dotenvy = "0.15.7" prost = "0.13.5" serde = { version = "1.0.218", features = ["derive"] } serde_json = "1.0.140" -sqlx = { version = "0.8.3", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "time"] } +sqlx = { version = "0.8.3", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "time", "uuid"] } tokio = { version = "1.43.0", features = ["full", "macros"] } tonic = "0.12.3" tonic-reflection = "0.12.3" @@ -24,6 +24,9 @@ thiserror = "2.0.12" dashmap = "6.1.0" lazy_static = "1.5.0" regex = "1.11.1" +bcrypt = "0.17.0" +validator = { version = "0.20.0", features = ["derive"] } +uuid = { version = "1.16.0", features = ["v4"] } [lib] name = "server" diff --git a/server/migrations/20250324192805_auth.sql b/server/migrations/20250324192805_auth.sql new file mode 100644 index 0000000..9cdc97f --- /dev/null +++ b/server/migrations/20250324192805_auth.sql @@ -0,0 +1,38 @@ +-- Add migration script here + +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(255) NOT NULL UNIQUE, + email VARCHAR(255) UNIQUE, + password_hash VARCHAR(255), + role VARCHAR(20) NOT NULL DEFAULT 'accountant', + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- Add an index for faster lookups +CREATE INDEX idx_users_email_username ON users(email, username); + +ALTER TABLE users +ADD CONSTRAINT valid_roles CHECK (role IN ( + 'admin', + 'moderator', + 'accountant', + 'viewer' +)); + +-- Create JWT sessions table +CREATE TABLE user_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + jwt_token TEXT NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + revoked BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Add indexes +CREATE INDEX idx_sessions_user ON user_sessions(user_id); +CREATE INDEX idx_sessions_expires ON user_sessions(expires_at); + + + diff --git a/server/src/auth/handlers.rs b/server/src/auth/handlers.rs new file mode 100644 index 0000000..f20cc8a --- /dev/null +++ b/server/src/auth/handlers.rs @@ -0,0 +1,5 @@ +// src/auth/handlers.rs + +pub mod register; + +pub use register::*; diff --git a/server/src/auth/handlers/register.rs b/server/src/auth/handlers/register.rs new file mode 100644 index 0000000..c4692d1 --- /dev/null +++ b/server/src/auth/handlers/register.rs @@ -0,0 +1,61 @@ +use bcrypt::{hash, DEFAULT_COST}; +use tonic::{Request, Response, Status}; +use uuid::Uuid; +use crate::{auth::{models::{RegisterRequest, AuthResponse, AuthError}, db::PgPool}}; + +pub struct AuthService { + pool: PgPool, +} + +impl AuthService { + pub fn new(pool: PgPool) -> Self { + Self { pool } + } +} + +#[tonic::async_trait] +impl multieko2::auth::auth_service_server::AuthService for AuthService { + async fn register( + &self, + request: Request, + ) -> Result, Status> { + let payload = request.into_inner(); + + // Validate passwords match + if payload.password != payload.password_confirmation { + return Err(Status::invalid_argument(AuthError::PasswordMismatch.to_string())); + } + + // Hash password + let password_hash = hash(payload.password, DEFAULT_COST) + .map_err(|e| Status::internal(AuthError::HashingError(e.to_string()).to_string()))?; + + // Insert user + let user = sqlx::query!( + r#" + INSERT INTO users (username, email, password_hash, role) + VALUES ($1, $2, $3, 'accountant') + RETURNING id, username, email, role + "#, + payload.username, + payload.email, + password_hash + ) + .fetch_one(&self.pool) + .await + .map_err(|e| { + if e.to_string().contains("duplicate key") { + Status::already_exists(AuthError::UserExists.to_string()) + } else { + Status::internal(AuthError::DatabaseError(e.to_string()).to_string()) + } + })?; + + Ok(Response::new(multieko2::auth::AuthResponse { + id: user.id.to_string(), + username: user.username, + email: user.email, + role: user.role, + })) + } +} diff --git a/server/src/auth/mod.rs b/server/src/auth/mod.rs new file mode 100644 index 0000000..a3529b4 --- /dev/null +++ b/server/src/auth/mod.rs @@ -0,0 +1,5 @@ +// src/auth/mod.rs + +pub mod models; +pub mod handlers; + diff --git a/server/src/auth/models.rs b/server/src/auth/models.rs new file mode 100644 index 0000000..51fee0e --- /dev/null +++ b/server/src/auth/models.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use validator::Validate; + +#[derive(Debug, Validate, Deserialize)] +pub struct RegisterRequest { + #[validate(length(min = 3, max = 30))] + pub username: String, + #[validate(email)] + pub email: String, + #[validate(length(min = 8))] + pub password: String, + pub password_confirmation: String, +} + +#[derive(Debug, Serialize)] +pub struct AuthResponse { + pub id: Uuid, + pub username: String, + pub email: String, + pub role: String, +} + +#[derive(Debug, thiserror::Error)] +pub enum AuthError { + #[error("Passwords do not match")] + PasswordMismatch, + #[error("User already exists")] + UserExists, + #[error("Database error: {0}")] + DatabaseError(String), + #[error("Hashing error: {0}")] + HashingError(String), +} diff --git a/server/src/lib.rs b/server/src/lib.rs index fe0295c..42a848f 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,5 +1,6 @@ // src/lib.rs pub mod db; +pub mod auth; pub mod server; pub mod adresar; pub mod uctovnictvo;