From fec9c546cef9bec25eeade19f49f6870c668e7f7 Mon Sep 17 00:00:00 2001 From: Speedy_Lex <78314533+speedy-lex@users.noreply.github.com> Date: Thu, 18 Sep 2025 21:06:04 +0200 Subject: [PATCH 1/4] database stuff --- Cargo.lock | 408 +++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + schema.sql | 123 +++++++++++++++ src/app.rs | 36 ++++- src/app/auth.rs | 74 +++++++-- src/main.rs | 2 +- 6 files changed, 620 insertions(+), 26 deletions(-) create mode 100644 schema.sql diff --git a/Cargo.lock b/Cargo.lock index e291386..a09fa36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,17 @@ dependencies = [ "libc", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -97,11 +108,14 @@ name = "backend" version = "0.1.0" dependencies = [ "axum", + "base64", "dotenvy", "rand", "serde", "serde_with", "tokio", + "tokio-postgres", + "uuid", ] [[package]] @@ -131,12 +145,27 @@ version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.10.1" @@ -177,6 +206,25 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.20.11" @@ -222,6 +270,17 @@ dependencies = [ "serde", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -240,6 +299,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "find-msvc-tools" version = "0.1.1" @@ -268,6 +333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -276,6 +342,23 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -289,9 +372,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-macro", + "futures-sink", "futures-task", "pin-project-lite", "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", ] [[package]] @@ -330,6 +426,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.3.1" @@ -498,6 +603,27 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.28" @@ -510,6 +636,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.5" @@ -539,7 +675,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -572,12 +708,53 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -590,6 +767,36 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "postgres-protocol" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", + "uuid", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -658,6 +865,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + [[package]] name = "ref-cast" version = "1.0.24" @@ -720,6 +936,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.225" @@ -818,12 +1040,29 @@ dependencies = [ "syn", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.11" @@ -836,6 +1075,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.0" @@ -843,7 +1092,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", ] [[package]] @@ -852,6 +1112,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.106" @@ -899,6 +1165,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.47.1" @@ -906,14 +1187,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", + "bytes", "io-uring", "libc", "mio", "pin-project-lite", "slab", - "socket2", + "socket2 0.6.0", "tokio-macros", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -927,6 +1209,45 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-postgres" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand", + "socket2 0.5.10", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.2" @@ -975,12 +1296,55 @@ dependencies = [ "once_cell", ] +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1005,6 +1369,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.103" @@ -1064,6 +1434,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + [[package]] name = "windows-core" version = "0.62.0" @@ -1123,6 +1514,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index 57738c7..e45cac1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,11 @@ edition = "2024" [dependencies] axum = "0.8.4" +base64 = "0.22.1" dotenvy = "0.15.7" rand = "0.9.2" serde = { version = "1.0.225", features = ["derive"] } serde_with = { version = "3.14.0", features = ["base64"] } tokio = { version = "1.47.1", features = ["rt-multi-thread"] } +tokio-postgres = { version = "0.7.13", features = ["with-uuid-1"] } +uuid = "1.18.1" diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..94cf392 --- /dev/null +++ b/schema.sql @@ -0,0 +1,123 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- ---------------------------- +-- Users (auth) +-- ---------------------------- +CREATE TABLE public.users ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + email text NOT NULL UNIQUE, + password_hash text NOT NULL, -- hashed password + created_at timestamp with time zone DEFAULT now(), + is_active boolean DEFAULT true, + last_login timestamp with time zone, + CONSTRAINT users_pkey PRIMARY KEY (id) +); + +-- ---------------------------- +-- Private user accounts (profile) +-- ---------------------------- +CREATE TABLE public.user_accounts ( + id uuid NOT NULL, -- use same UUID as users + username text NOT NULL UNIQUE, + created_at timestamp with time zone DEFAULT now(), + is_moderator boolean DEFAULT false, + is_admin boolean DEFAULT false, + support_balance numeric DEFAULT 0, -- donation total (estimated) + CONSTRAINT user_accounts_pkey PRIMARY KEY (id), + CONSTRAINT user_accounts_user_id_fkey FOREIGN KEY (id) REFERENCES public.users(id) +); + +-- ---------------------------- +-- Public channels +-- ---------------------------- +CREATE TABLE public.public_channels ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + user_id uuid NOT NULL, -- channel's owner id + display_name text NOT NULL, + handle text NOT NULL, -- unique handle (@handle) + avatar_url text, + banner_url text, + created_at timestamp with time zone DEFAULT now(), + CONSTRAINT public_channels_pkey PRIMARY KEY (id), + CONSTRAINT public_channels_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.user_accounts(id), + CONSTRAINT public_channels_handle_unique UNIQUE (handle) + CONSTRAINT public_channels_unique UNIQUE (user_id) -- one channel per user +); + +-- ---------------------------- +-- Videos +-- ---------------------------- +-- enum for status +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'video_status') THEN + CREATE TYPE video_status AS ENUM ('pending','accepted','refused'); + END IF; +END $$; + +CREATE TABLE public.videos ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + channel_id uuid NOT NULL, -- video's channel + title text NOT NULL, + description text, + storage_path text, + thumbnail text, + duration interval, -- or "text" type for formatted duration + themes text[], + created_at timestamp with time zone DEFAULT now(), + quality_score double precision DEFAULT 0, + + status video_status NOT NULL DEFAULT 'pending', + refusal_reason text, -- reason for refusal if status = 'refused' + + is_private boolean DEFAULT false, + allow_download boolean DEFAULT false, + + CONSTRAINT videos_pkey PRIMARY KEY (id), + CONSTRAINT videos_channel_id_fkey FOREIGN KEY (channel_id) REFERENCES public.public_channels(id) +); + +-- ---------------------------- +-- Subscriptions +-- ---------------------------- +CREATE TABLE public.subscriptions ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + subscriber_id uuid NOT NULL, -- account id of the subscriber (private account) + channel_id uuid NOT NULL, -- creator's public channel id + notifications boolean DEFAULT true, + no_ping boolean DEFAULT false, + created_at timestamp with time zone DEFAULT now(), + CONSTRAINT subscriptions_pkey PRIMARY KEY (id), + CONSTRAINT subscriptions_subscriber_fkey FOREIGN KEY (subscriber_id) REFERENCES public.user_accounts(id), + CONSTRAINT subscriptions_channel_fkey FOREIGN KEY (channel_id) REFERENCES public.public_channels(id), + CONSTRAINT subscriptions_unique UNIQUE (subscriber_id, channel_id) +); + +-- ---------------------------- +-- Ratings +-- ---------------------------- +CREATE TABLE public.ratings ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + video_id uuid NOT NULL, + user_id uuid NOT NULL, + score smallint NOT NULL CHECK (score >= 1 AND score <= 5), + created_at timestamp with time zone DEFAULT now(), + CONSTRAINT ratings_pkey PRIMARY KEY (id), + CONSTRAINT ratings_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.user_accounts(id), + CONSTRAINT ratings_video_id_fkey FOREIGN KEY (video_id) REFERENCES public.videos(id), + CONSTRAINT ratings_unique UNIQUE (video_id, user_id) +); + +-- ---------------------------- +-- Donations +-- ---------------------------- +CREATE TABLE public.donations ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + supporter_id uuid NOT NULL, -- account id of the supporter (private account) + channel_id uuid NOT NULL, -- creator's public channel id + amount numeric NOT NULL CHECK (amount > 0), + method text CHECK (method = ANY (ARRAY['donation', 'ad'])), -- donation or ad revenue + created_at timestamp with time zone DEFAULT now(), + CONSTRAINT donations_pkey PRIMARY KEY (id), + CONSTRAINT donations_supporter_fkey FOREIGN KEY (supporter_id) REFERENCES public.user_accounts(id), + CONSTRAINT donations_channel_fkey FOREIGN KEY (channel_id) REFERENCES public.public_channels(id) +); diff --git a/src/app.rs b/src/app.rs index a4278af..c6bd843 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,19 +1,47 @@ -use std::sync::Arc; +use std::{net::{IpAddr, Ipv4Addr}, sync::Arc}; use axum::{Json, Router, extract::State, routing::post}; +use base64::{alphabet::STANDARD, engine::{GeneralPurpose, GeneralPurposeConfig}}; use serde::{Deserialize, Serialize}; use tokio::sync::{Mutex, RwLock}; +use tokio_postgres::{config::SslMode, Config}; use crate::app::auth::{AuthState, Token, router}; mod auth; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] struct AppState { value: Arc>, auth: Arc>, + base64: GeneralPurpose, } impl AppState { + async fn new() -> Self { + let mut cfg = Config::new(); + + cfg + .hostaddr(IpAddr::V4(Ipv4Addr::LOCALHOST)) + .ssl_mode(SslMode::Disable); + + if let Ok(user) = dotenvy::var("POSTGRES_USER") { + cfg.user(user); + } else { + cfg.user("fairplay-test"); + } + if let Ok(password) = dotenvy::var("POSTGRES_PASSWORD") { + cfg.password(password); + } else { + cfg.password("fairplay"); + } + cfg.dbname("fairplay-test"); + + Self { + value: Arc::new(RwLock::new(0.0)), + auth: Arc::new(Mutex::new(AuthState::new(&cfg).await)), + base64: GeneralPurpose::new(&STANDARD, GeneralPurposeConfig::new()) + } + } async fn validate_token(&self, token: &Token) -> bool { self.auth.lock().await.tokens.contains_key(token) } @@ -40,9 +68,9 @@ async fn get_value(State(state): State) -> Json { Json(*state.value.read().await) } -pub fn new_app() -> Router { +pub async fn new_app() -> Router { Router::new() .route("/value", post(put_value).get(get_value)) .nest("/auth", router()) - .with_state(AppState::default()) + .with_state(AppState::new().await) } diff --git a/src/app/auth.rs b/src/app/auth.rs index f7a67ec..5140710 100644 --- a/src/app/auth.rs +++ b/src/app/auth.rs @@ -1,9 +1,12 @@ use std::collections::HashMap; use axum::{Json, Router, extract::State, routing::post}; +use base64::Engine; use rand::Rng; use serde::{Deserialize, Serialize}; use serde_with::{base64::Base64, serde_as}; +use tokio_postgres::{Client, Config, NoTls}; +use uuid::Uuid; use super::AppState; @@ -11,10 +14,20 @@ use super::AppState; #[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub struct Token(#[serde_as(as = "Base64")] pub [u8; 32]); -#[derive(Debug, Default, Clone)] +#[derive(Debug)] pub struct AuthState { - db: HashMap>, - pub tokens: HashMap, + db: Client, + pub tokens: HashMap, +} +impl AuthState { + pub async fn new(cfg: &Config) -> Self { + let (client, connection) = cfg.connect(NoTls) + .await.unwrap(); + tokio::spawn(async { + connection.await.unwrap(); // run the connection on a bg task + }); + Self { db: client, tokens: Default::default() } + } } pub fn router() -> Router { @@ -27,33 +40,60 @@ pub fn router() -> Router { #[derive(Serialize, Deserialize)] struct RegisterRequest { username: String, + email: String, #[serde_as(as = "Base64")] secret: Vec, } async fn register( State(state): State, Json(request): Json, -) -> Json> { - let mut lock = state.auth.lock().await; - if lock.db.contains_key(&request.username) { - return Json(Err("ALREADY_EXISTS")); +) -> Json> { + let lock = state.auth.lock().await; + let res = lock.db.execute( + "WITH new_user AS ( + INSERT INTO public.users (email, password_hash) + VALUES ($3, $2) + RETURNING id + ) + INSERT INTO public.user_accounts (id, username) + SELECT id, $1 + FROM new_user;", + &[&request.username, &state.base64.encode(request.secret), &request.email]).await; + let res = res.map_err(|x| { Json(Err(x.to_string())) }); + if let Err(e) = res { + return e; } - lock.db.insert(request.username, request.secret); Json(Ok(())) } -type LoginRequest = RegisterRequest; +#[serde_as] +#[derive(Serialize, Deserialize)] +struct LoginRequest { + email: String, + #[serde_as(as = "Base64")] + secret: Vec, +} + async fn login( State(state): State, Json(request): Json, -) -> Json> { +) -> Json> { let mut lock = state.auth.lock().await; - match lock.db.get(&request.username) { - Some(x) if *x == request.secret => { - let token = Token(rand::rng().random::<[u8; 32]>()); - assert!(lock.tokens.insert(token, request.username).is_none()); // we should never see a token collision - Json(Ok(token)) - } - None | Some(_) => Json(Err("INVALID_CREDENTIALS")), + let res = lock.db.query_one( + "SELECT id, password_hash + FROM public.users + WHERE email = $1;", + &[&request.email]).await; + let res = res.map_err(|x| { Json(Err(x.to_string())) }); + let row = match res { + Ok(row) => row, + Err(err) => return err, + }; + if state.base64.decode(row.get::<&str, String>("password_hash")).unwrap() == request.secret { + let token = Token(rand::rng().random::<[u8; 32]>()); + assert!(lock.tokens.insert(token, row.get("id")).is_none()); // we should never see a token collision + Json(Ok(token)) + } else { + Json(Err("INVALID_CREDENTIALS".to_string())) } } diff --git a/src/main.rs b/src/main.rs index f00c4e2..463cead 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ mod app; #[tokio::main] async fn main() { let _ = dotenvy::dotenv_override(); // it doesn't matter if there isnt a .env - let app = new_app(); + let app = new_app().await; let port = dotenvy::var("HTTP_PORT") .map(|x| x.parse().unwrap()) From 5d974f90eb400f4d09c42049039c88a2f949671d Mon Sep 17 00:00:00 2001 From: Speedy_Lex <78314533+speedy-lex@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:04:01 +0200 Subject: [PATCH 2/4] use bytea for password hashes --- schema.sql | 5 +++-- src/app.rs | 3 --- src/app/auth.rs | 5 ++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/schema.sql b/schema.sql index 94cf392..cd143e9 100644 --- a/schema.sql +++ b/schema.sql @@ -6,11 +6,12 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE TABLE public.users ( id uuid NOT NULL DEFAULT uuid_generate_v4(), email text NOT NULL UNIQUE, - password_hash text NOT NULL, -- hashed password + password_hash bytea NOT NULL, -- hashed password created_at timestamp with time zone DEFAULT now(), is_active boolean DEFAULT true, last_login timestamp with time zone, CONSTRAINT users_pkey PRIMARY KEY (id) + -- CONSTRAINT password_hash_length CHECK (octet_length(password_hash) = 32) ); -- ---------------------------- @@ -40,7 +41,7 @@ CREATE TABLE public.public_channels ( created_at timestamp with time zone DEFAULT now(), CONSTRAINT public_channels_pkey PRIMARY KEY (id), CONSTRAINT public_channels_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.user_accounts(id), - CONSTRAINT public_channels_handle_unique UNIQUE (handle) + CONSTRAINT public_channels_handle_unique UNIQUE (handle), CONSTRAINT public_channels_unique UNIQUE (user_id) -- one channel per user ); diff --git a/src/app.rs b/src/app.rs index c6bd843..e5e9e32 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,6 @@ use std::{net::{IpAddr, Ipv4Addr}, sync::Arc}; use axum::{Json, Router, extract::State, routing::post}; -use base64::{alphabet::STANDARD, engine::{GeneralPurpose, GeneralPurposeConfig}}; use serde::{Deserialize, Serialize}; use tokio::sync::{Mutex, RwLock}; use tokio_postgres::{config::SslMode, Config}; @@ -14,7 +13,6 @@ mod auth; struct AppState { value: Arc>, auth: Arc>, - base64: GeneralPurpose, } impl AppState { async fn new() -> Self { @@ -39,7 +37,6 @@ impl AppState { Self { value: Arc::new(RwLock::new(0.0)), auth: Arc::new(Mutex::new(AuthState::new(&cfg).await)), - base64: GeneralPurpose::new(&STANDARD, GeneralPurposeConfig::new()) } } async fn validate_token(&self, token: &Token) -> bool { diff --git a/src/app/auth.rs b/src/app/auth.rs index 5140710..575e894 100644 --- a/src/app/auth.rs +++ b/src/app/auth.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use axum::{Json, Router, extract::State, routing::post}; -use base64::Engine; use rand::Rng; use serde::{Deserialize, Serialize}; use serde_with::{base64::Base64, serde_as}; @@ -58,7 +57,7 @@ async fn register( INSERT INTO public.user_accounts (id, username) SELECT id, $1 FROM new_user;", - &[&request.username, &state.base64.encode(request.secret), &request.email]).await; + &[&request.username, &request.secret, &request.email]).await; let res = res.map_err(|x| { Json(Err(x.to_string())) }); if let Err(e) = res { return e; @@ -89,7 +88,7 @@ async fn login( Ok(row) => row, Err(err) => return err, }; - if state.base64.decode(row.get::<&str, String>("password_hash")).unwrap() == request.secret { + if row.get::<_, &[u8]>("password_hash") == request.secret { let token = Token(rand::rng().random::<[u8; 32]>()); assert!(lock.tokens.insert(token, row.get("id")).is_none()); // we should never see a token collision Json(Ok(token)) From 45055de16a1ea55d0da2c6ab54e608379306eaca Mon Sep 17 00:00:00 2001 From: Speedy_Lex <78314533+speedy-lex@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:05:00 +0200 Subject: [PATCH 3/4] format code --- src/app.rs | 10 ++++++---- src/app/auth.rs | 32 +++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/app.rs b/src/app.rs index e5e9e32..8c06230 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,9 +1,12 @@ -use std::{net::{IpAddr, Ipv4Addr}, sync::Arc}; +use std::{ + net::{IpAddr, Ipv4Addr}, + sync::Arc, +}; use axum::{Json, Router, extract::State, routing::post}; use serde::{Deserialize, Serialize}; use tokio::sync::{Mutex, RwLock}; -use tokio_postgres::{config::SslMode, Config}; +use tokio_postgres::{Config, config::SslMode}; use crate::app::auth::{AuthState, Token, router}; @@ -18,8 +21,7 @@ impl AppState { async fn new() -> Self { let mut cfg = Config::new(); - cfg - .hostaddr(IpAddr::V4(Ipv4Addr::LOCALHOST)) + cfg.hostaddr(IpAddr::V4(Ipv4Addr::LOCALHOST)) .ssl_mode(SslMode::Disable); if let Ok(user) = dotenvy::var("POSTGRES_USER") { diff --git a/src/app/auth.rs b/src/app/auth.rs index 575e894..46c33c4 100644 --- a/src/app/auth.rs +++ b/src/app/auth.rs @@ -20,12 +20,14 @@ pub struct AuthState { } impl AuthState { pub async fn new(cfg: &Config) -> Self { - let (client, connection) = cfg.connect(NoTls) - .await.unwrap(); + let (client, connection) = cfg.connect(NoTls).await.unwrap(); tokio::spawn(async { connection.await.unwrap(); // run the connection on a bg task }); - Self { db: client, tokens: Default::default() } + Self { + db: client, + tokens: Default::default(), + } } } @@ -48,8 +50,10 @@ async fn register( Json(request): Json, ) -> Json> { let lock = state.auth.lock().await; - let res = lock.db.execute( - "WITH new_user AS ( + let res = lock + .db + .execute( + "WITH new_user AS ( INSERT INTO public.users (email, password_hash) VALUES ($3, $2) RETURNING id @@ -57,8 +61,10 @@ async fn register( INSERT INTO public.user_accounts (id, username) SELECT id, $1 FROM new_user;", - &[&request.username, &request.secret, &request.email]).await; - let res = res.map_err(|x| { Json(Err(x.to_string())) }); + &[&request.username, &request.secret, &request.email], + ) + .await; + let res = res.map_err(|x| Json(Err(x.to_string()))); if let Err(e) = res { return e; } @@ -78,12 +84,16 @@ async fn login( Json(request): Json, ) -> Json> { let mut lock = state.auth.lock().await; - let res = lock.db.query_one( - "SELECT id, password_hash + let res = lock + .db + .query_one( + "SELECT id, password_hash FROM public.users WHERE email = $1;", - &[&request.email]).await; - let res = res.map_err(|x| { Json(Err(x.to_string())) }); + &[&request.email], + ) + .await; + let res = res.map_err(|x| Json(Err(x.to_string()))); let row = match res { Ok(row) => row, Err(err) => return err, From f68268ced48f50acab3bee895317f785d3ad6beb Mon Sep 17 00:00:00 2001 From: Speedy_Lex <78314533+speedy-lex@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:45:57 +0200 Subject: [PATCH 4/4] hash passwords with argon2 --- Cargo.lock | 65 ++++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 1 + schema.sql | 3 +-- src/app.rs | 3 +++ src/app/auth.rs | 43 +++++++++++++++++++++++--------- 5 files changed, 99 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a09fa36..779d637 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,18 @@ dependencies = [ "libc", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -107,6 +119,7 @@ dependencies = [ name = "backend" version = "0.1.0" dependencies = [ + "argon2", "axum", "base64", "dotenvy", @@ -139,12 +152,27 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + [[package]] name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -390,6 +418,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -731,6 +770,17 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -843,7 +893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.3", ] [[package]] @@ -853,7 +903,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", ] [[package]] @@ -862,7 +921,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom", + "getrandom 0.3.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e45cac1..e57cd9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +argon2 = { version = "0.5.3", features = ["std"] } axum = "0.8.4" base64 = "0.22.1" dotenvy = "0.15.7" diff --git a/schema.sql b/schema.sql index cd143e9..8743629 100644 --- a/schema.sql +++ b/schema.sql @@ -6,12 +6,11 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE TABLE public.users ( id uuid NOT NULL DEFAULT uuid_generate_v4(), email text NOT NULL UNIQUE, - password_hash bytea NOT NULL, -- hashed password + password_hash text NOT NULL, -- hashed password created_at timestamp with time zone DEFAULT now(), is_active boolean DEFAULT true, last_login timestamp with time zone, CONSTRAINT users_pkey PRIMARY KEY (id) - -- CONSTRAINT password_hash_length CHECK (octet_length(password_hash) = 32) ); -- ---------------------------- diff --git a/src/app.rs b/src/app.rs index 8c06230..f6ba671 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,6 +3,7 @@ use std::{ sync::Arc, }; +use argon2::{Algorithm, Argon2, Params, Version}; use axum::{Json, Router, extract::State, routing::post}; use serde::{Deserialize, Serialize}; use tokio::sync::{Mutex, RwLock}; @@ -16,6 +17,7 @@ mod auth; struct AppState { value: Arc>, auth: Arc>, + hasher: Argon2<'static>, } impl AppState { async fn new() -> Self { @@ -39,6 +41,7 @@ impl AppState { Self { value: Arc::new(RwLock::new(0.0)), auth: Arc::new(Mutex::new(AuthState::new(&cfg).await)), + hasher: Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::DEFAULT), } } async fn validate_token(&self, token: &Token) -> bool { diff --git a/src/app/auth.rs b/src/app/auth.rs index 46c33c4..6541204 100644 --- a/src/app/auth.rs +++ b/src/app/auth.rs @@ -1,5 +1,9 @@ use std::collections::HashMap; +use argon2::{ + PasswordVerifier, + password_hash::{PasswordHashString, PasswordHasher, SaltString, rand_core::OsRng}, +}; use axum::{Json, Router, extract::State, routing::post}; use rand::Rng; use serde::{Deserialize, Serialize}; @@ -49,19 +53,26 @@ async fn register( State(state): State, Json(request): Json, ) -> Json> { + let salt = SaltString::generate(&mut OsRng); + let hash = state.hasher.hash_password(&request.secret, &salt).unwrap(); + let lock = state.auth.lock().await; let res = lock .db .execute( "WITH new_user AS ( - INSERT INTO public.users (email, password_hash) - VALUES ($3, $2) - RETURNING id - ) - INSERT INTO public.user_accounts (id, username) - SELECT id, $1 - FROM new_user;", - &[&request.username, &request.secret, &request.email], + INSERT INTO public.users (password_hash, email) + VALUES ($2, $3) + RETURNING id + ) + INSERT INTO public.user_accounts (id, username) + SELECT id, $1 + FROM new_user;", + &[ + &request.username, + &hash.serialize().as_str(), + &request.email, + ], ) .await; let res = res.map_err(|x| Json(Err(x.to_string()))); @@ -88,8 +99,8 @@ async fn login( .db .query_one( "SELECT id, password_hash - FROM public.users - WHERE email = $1;", + FROM public.users + WHERE email = $1;", &[&request.email], ) .await; @@ -98,7 +109,17 @@ async fn login( Ok(row) => row, Err(err) => return err, }; - if row.get::<_, &[u8]>("password_hash") == request.secret { + + if state + .hasher + .verify_password( + &request.secret, + &PasswordHashString::new(row.get::<_, &str>("password_hash")) + .unwrap() + .password_hash(), + ) + .is_ok() + { let token = Token(rand::rng().random::<[u8; 32]>()); assert!(lock.tokens.insert(token, row.get("id")).is_none()); // we should never see a token collision Json(Ok(token))