diff --git a/.woodpecker/build-and-publish.yml b/.woodpecker/build-and-publish.yml index 7f7096b..c0f367a 100644 --- a/.woodpecker/build-and-publish.yml +++ b/.woodpecker/build-and-publish.yml @@ -1,25 +1,8 @@ +when: + - event: push + branch: main + steps: - - name: build-x86_64 - image: rust:1.88-bookworm - commands: - - cargo build --release - when: - - event: push - - - name: build-arm64 - image: rust:1.88-bookworm - commands: - - dpkg --add-architecture arm64 - - apt-get update -y && apt-get install -y crossbuild-essential-arm64 libssl-dev:arm64 - - rustup target add aarch64-unknown-linux-gnu - - cargo build --target aarch64-unknown-linux-gnu --release - environment: - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc - PKG_CONFIG_ALLOW_CROSS: 1 - PKG_CONFIG_PATH: /usr/aarch64-linux-gnu/lib/pkgconfig - when: - - event: push - - name: container-build-and-publish image: docker commands: @@ -30,20 +13,3 @@ steps: from_secret: docker_password volumes: - /var/run/podman/podman.sock:/var/run/docker.sock - when: - - branch: main - event: push - - - name: container-build-and-publish-staging - image: docker - commands: - - docker login --username radical --password $PASSWORD git.gorb.app - - docker buildx build --platform linux/amd64,linux/arm64 --rm --push -t git.gorb.app/gorb/backend:staging . - environment: - PASSWORD: - from_secret: docker_password - volumes: - - /var/run/podman/podman.sock:/var/run/docker.sock - when: - - branch: staging - event: push diff --git a/.woodpecker/publish-docs.yml b/.woodpecker/publish-docs.yml deleted file mode 100644 index 7744dc7..0000000 --- a/.woodpecker/publish-docs.yml +++ /dev/null @@ -1,19 +0,0 @@ -when: - - event: push - branch: main - -steps: - - name: build-docs - image: rust:1.88-bookworm - commands: - - cargo doc --release --no-deps - - - name: publish-docs - image: debian:12 - commands: - - apt update -y && apt install -y rsync openssh-client - - printf "Host *\n StrictHostKeyChecking no" >> /etc/ssh/ssh_config - - ssh-agent bash -c "ssh-add <(echo '$KEY' | base64 -d) && rsync --archive --verbose --compress --hard-links --delete-during --partial --progress ./target/doc/ root@gorb.app:/var/www/docs.gorb.app/api && ssh root@gorb.app systemctl reload caddy.service" - environment: - KEY: - from_secret: ssh_key diff --git a/Cargo.toml b/Cargo.toml index cdbcc0f..18aa043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,56 +8,37 @@ strip = true lto = true codegen-units = 1 -# Speed up compilation to make dev bearable -[profile.dev] -debug = 0 -strip = "debuginfo" -codegen-units = 512 - [dependencies] -thiserror = "2.0.12" - -# CLI -clap = { version = "4.5", features = ["derive"] } -log = "0.4" - -# async -tokio = { version = "1.46", features = ["full"] } -futures-util = "0.3.31" - -# Data (de)serialization -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -toml = "0.9" -bytes = "1.10.1" - -# File Storage -bindet = "0.3.2" -bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } - -# Web Server -axum = { version = "0.8.4", features = ["multipart", "ws"] } -axum-extra = { version = "0.10.1", features = ["cookie", "typed-header"] } -tower-http = { version = "0.6.6", features = ["cors"] } -#socketioxide = { version = "0.17.2", features = ["state"] } -url = { version = "2.5", features = ["serde"] } -time = "0.3.41" - -# Database -uuid = { version = "1.17", features = ["serde", "v7"] } -redis = { version = "0.32", features= ["tokio-comp"] } -deadpool = "0.12" -diesel = { version = "2.2", features = ["uuid", "chrono"], default-features = false } -diesel-async = { version = "0.6", features = ["deadpool", "postgres", "async-connection-wrapper"] } -diesel_migrations = { version = "2.2.0", features = ["postgres"] } - -# Authentication +actix-cors = "0.7.1" +actix-web = "4.11" argon2 = { version = "0.5.3", features = ["std"] } +clap = { version = "4.5", features = ["derive"] } +futures = "0.3" getrandom = "0.3" hex = "0.4" +log = "0.4" regex = "1.11" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +simple_logger = "5.0.0" +redis = { version = "0.31.0", features= ["tokio-comp"] } +tokio-tungstenite = { version = "0.26", features = ["native-tls", "url"] } +toml = "0.8" +url = { version = "2.5", features = ["serde"] } +uuid = { version = "1.16", features = ["serde", "v7"] } random-string = "1.1" -lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } -chrono = { version = "0.4.41", features = ["serde"] } -tracing-subscriber = "0.3.19" -rand = "0.9.1" +actix-ws = "0.3.0" +futures-util = "0.3.31" +bunny-api-tokio = "0.3.0" +bindet = "0.3.2" +deadpool = "0.12" +diesel = { version = "2.2", features = ["uuid"] } +diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-connection-wrapper"] } +diesel_migrations = { version = "2.2.0", features = ["postgres"] } +thiserror = "2.0.12" +actix-multipart = "0.7.2" +actix-files = "0.6.6" + +[dependencies.tokio] +version = "1.44" +features = ["full"] diff --git a/Dockerfile b/Dockerfile index 25795a9..f76653a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,16 @@ -FROM --platform=linux/amd64 debian:12-slim AS prep +FROM rust:bookworm AS builder WORKDIR /src -COPY target/release/backend backend-amd64 -COPY target/aarch64-unknown-linux-gnu/release/backend backend-arm64 +COPY . . + +RUN cargo build --release FROM debian:12-slim -ARG TARGETARCH +RUN apt update && apt install libssl3 && rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* -RUN apt update -y && apt install libssl3 ca-certificates -y && rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* - -COPY --from=prep /src/backend-${TARGETARCH} /usr/bin/gorb-backend +COPY --from=builder /src/target/release/backend /usr/bin/gorb-backend COPY entrypoint.sh /usr/bin/entrypoint.sh @@ -19,8 +18,7 @@ RUN useradd --create-home --home-dir /gorb gorb USER gorb -ENV WEB_FRONTEND_URL=https://gorb.app/web/ \ -WEB_BASE_PATH=/api \ +ENV WEB_URL=http://localhost:8080 \ DATABASE_USERNAME=gorb \ DATABASE_PASSWORD=gorb \ DATABASE=gorb \ @@ -28,14 +26,9 @@ DATABASE_HOST=database \ DATABASE_PORT=5432 \ CACHE_DB_HOST=valkey \ CACHE_DB_PORT=6379 \ -BUNNY_API_KEY=your_storage_zone_password_here \ -BUNNY_ENDPOINT=Frankfurt \ -BUNNY_ZONE=gorb \ -BUNNY_CDN_URL=https://cdn.gorb.app \ -MAIL_ADDRESS=noreply@gorb.app \ -MAIL_TLS=tls \ -SMTP_SERVER=mail.gorb.app \ -SMTP_USERNAME=your_smtp_username \ -SMTP_PASSWORD=your_smtp_password +BUNNY_API_KEY= \ +BUNNY_ENDPOINT= \ +BUNNY_ZONE= \ +BUNNY_CDN_URL= ENTRYPOINT ["/usr/bin/entrypoint.sh"] diff --git a/build.rs b/build.rs index 55fb863..3a8149e 100644 --- a/build.rs +++ b/build.rs @@ -1,16 +1,3 @@ -use std::process::Command; - fn main() { println!("cargo:rerun-if-changed=migrations"); - - let git_short_hash = Command::new("git") - .args(["rev-parse", "--short", "HEAD"]) - .output() - .ok() - .and_then(|o| String::from_utf8(o.stdout).ok()) - .map(|s| s.trim().to_string()) // Trim newline - .unwrap_or_else(|| "UNKNOWN".to_string()); - - // Tell Cargo to set `GIT_SHORT_HASH` for the main compilation - println!("cargo:rustc-env=GIT_SHORT_HASH={git_short_hash}"); } diff --git a/compose.dev.yml b/compose.dev.yml index 93a1a85..af28fcc 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -18,21 +18,18 @@ services: - gorb-backend:/gorb environment: #- RUST_LOG=debug - - WEB_FRONTEND_URL=https://gorb.app/web/ + # This should be changed to the public URL of the server! + - WEB_URL=http://localhost:8080 - DATABASE_USERNAME=gorb - DATABASE_PASSWORD=gorb - DATABASE=gorb - DATABASE_HOST=database - DATABASE_PORT=5432 - - BUNNY_API_KEY=your_storage_zone_password_here - - BUNNY_ENDPOINT=Frankfurt - - BUNNY_ZONE=gorb - - BUNNY_CDN_URL=https://cdn.gorb.app - - MAIL_ADDRESS=Gorb - - MAIL_TLS=tls - - SMTP_SERVER=mail.gorb.app - - SMTP_USERNAME=your_smtp_username - - SMTP_PASSWORD=your_smtp_password + # These can be set to use a CDN, if they are not set then files will be stored locally + #- BUNNY_API_KEY=your_storage_zone_password_here + #- BUNNY_ENDPOINT=Frankfurt + #- BUNNY_ZONE=gorb + #- BUNNY_CDN_URL=https://cdn.gorb.app database: image: postgres:16 restart: always diff --git a/compose.yml b/compose.yml index b1dc07d..e3ad269 100644 --- a/compose.yml +++ b/compose.yml @@ -16,21 +16,18 @@ services: - gorb-backend:/gorb environment: #- RUST_LOG=debug - - WEB_FRONTEND_URL=https://gorb.app/web/ + # This should be changed to the public URL of the server! + - WEB_URL=http://localhost:8080 - DATABASE_USERNAME=gorb - DATABASE_PASSWORD=gorb - DATABASE=gorb - DATABASE_HOST=database - DATABASE_PORT=5432 - - BUNNY_API_KEY=your_storage_zone_password_here - - BUNNY_ENDPOINT=Frankfurt - - BUNNY_ZONE=gorb - - BUNNY_CDN_URL=https://cdn.gorb.app - - MAIL_ADDRESS=Gorb - - MAIL_TLS=tls - - SMTP_SERVER=mail.gorb.app - - SMTP_USERNAME=your_smtp_username - - SMTP_PASSWORD=your_smtp_password + # These can be set to use a CDN, if they are not set then files will be stored locally + #- BUNNY_API_KEY=your_storage_zone_password_here + #- BUNNY_ENDPOINT=Frankfurt + #- BUNNY_ZONE=gorb + #- BUNNY_CDN_URL=https://cdn.gorb.app database: image: postgres:16 restart: always diff --git a/entrypoint.sh b/entrypoint.sh index 38ba890..5cf3dc3 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -8,11 +8,14 @@ if [ ! -d "/gorb/logs" ]; then mkdir /gorb/logs fi +if [ ! -d "/gorb/data" ]; then + mkdir /gorb/data +fi + if [ ! -f "/gorb/config/config.toml" ]; then cat > /gorb/config/config.toml <> "/gorb/config/config.toml" <&1 | tee /gorb/logs/backend.log +/usr/bin/gorb-backend --config /gorb/config/config.toml --data-dir /gorb/data 2>&1 | tee /gorb/logs/backend.log diff --git a/migrations/2025-05-26-181536_add_channel_ordering/down.sql b/migrations/2025-05-26-181536_add_channel_ordering/down.sql deleted file mode 100644 index 0a70d35..0000000 --- a/migrations/2025-05-26-181536_add_channel_ordering/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE channels DROP COLUMN is_above; diff --git a/migrations/2025-05-26-181536_add_channel_ordering/up.sql b/migrations/2025-05-26-181536_add_channel_ordering/up.sql deleted file mode 100644 index e18e5e2..0000000 --- a/migrations/2025-05-26-181536_add_channel_ordering/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE channels ADD COLUMN is_above UUID UNIQUE REFERENCES channels(uuid) DEFAULT NULL; diff --git a/migrations/2025-05-27-105059_redo_role_ordering/down.sql b/migrations/2025-05-27-105059_redo_role_ordering/down.sql deleted file mode 100644 index 6b38e1e..0000000 --- a/migrations/2025-05-27-105059_redo_role_ordering/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE roles ADD COLUMN position int NOT NULL DEFAULT 0; -ALTER TABLE roles DROP COLUMN is_above; diff --git a/migrations/2025-05-27-105059_redo_role_ordering/up.sql b/migrations/2025-05-27-105059_redo_role_ordering/up.sql deleted file mode 100644 index d426ab7..0000000 --- a/migrations/2025-05-27-105059_redo_role_ordering/up.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Your SQL goes here -ALTER TABLE roles DROP COLUMN position; -ALTER TABLE roles ADD COLUMN is_above UUID UNIQUE REFERENCES roles(uuid) DEFAULT NULL; diff --git a/migrations/2025-05-27-162114_create_email_tokens/down.sql b/migrations/2025-05-27-162114_create_email_tokens/down.sql deleted file mode 100644 index f56c360..0000000 --- a/migrations/2025-05-27-162114_create_email_tokens/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE email_tokens; diff --git a/migrations/2025-05-27-162114_create_email_tokens/up.sql b/migrations/2025-05-27-162114_create_email_tokens/up.sql deleted file mode 100644 index 9761563..0000000 --- a/migrations/2025-05-27-162114_create_email_tokens/up.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Your SQL goes here -CREATE TABLE email_tokens ( - token VARCHAR(64) NOT NULL, - user_uuid uuid UNIQUE NOT NULL REFERENCES users(uuid), - created_at TIMESTAMPTZ NOT NULL, - PRIMARY KEY (token, user_uuid) -); diff --git a/migrations/2025-05-28-175918_create_password_reset_tokens/down.sql b/migrations/2025-05-28-175918_create_password_reset_tokens/down.sql deleted file mode 100644 index dcccc77..0000000 --- a/migrations/2025-05-28-175918_create_password_reset_tokens/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE password_reset_tokens; diff --git a/migrations/2025-05-28-175918_create_password_reset_tokens/up.sql b/migrations/2025-05-28-175918_create_password_reset_tokens/up.sql deleted file mode 100644 index f788b77..0000000 --- a/migrations/2025-05-28-175918_create_password_reset_tokens/up.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Your SQL goes here -CREATE TABLE password_reset_tokens ( - token VARCHAR(64) NOT NULL, - user_uuid uuid UNIQUE NOT NULL REFERENCES users(uuid), - created_at TIMESTAMPTZ NOT NULL, - PRIMARY KEY (token, user_uuid) -); diff --git a/migrations/2025-06-01-134036_add_pronouns_to_users/down.sql b/migrations/2025-06-01-134036_add_pronouns_to_users/down.sql deleted file mode 100644 index 32d891f..0000000 --- a/migrations/2025-06-01-134036_add_pronouns_to_users/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE users DROP COLUMN pronouns; diff --git a/migrations/2025-06-01-134036_add_pronouns_to_users/up.sql b/migrations/2025-06-01-134036_add_pronouns_to_users/up.sql deleted file mode 100644 index 90807bb..0000000 --- a/migrations/2025-06-01-134036_add_pronouns_to_users/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE users ADD COLUMN pronouns VARCHAR(32) DEFAULT NULL; diff --git a/migrations/2025-06-01-143713_add_about_to_users/down.sql b/migrations/2025-06-01-143713_add_about_to_users/down.sql deleted file mode 100644 index de48d07..0000000 --- a/migrations/2025-06-01-143713_add_about_to_users/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE users DROP COLUMN about; \ No newline at end of file diff --git a/migrations/2025-06-01-143713_add_about_to_users/up.sql b/migrations/2025-06-01-143713_add_about_to_users/up.sql deleted file mode 100644 index 54b5449..0000000 --- a/migrations/2025-06-01-143713_add_about_to_users/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE users ADD COLUMN about VARCHAR(200) DEFAULT NULL; diff --git a/migrations/2025-06-03-103311_remove_email_tokens/down.sql b/migrations/2025-06-03-103311_remove_email_tokens/down.sql deleted file mode 100644 index e8f0350..0000000 --- a/migrations/2025-06-03-103311_remove_email_tokens/down.sql +++ /dev/null @@ -1,7 +0,0 @@ --- This file should undo anything in `up.sql` -CREATE TABLE email_tokens ( - token VARCHAR(64) NOT NULL, - user_uuid uuid UNIQUE NOT NULL REFERENCES users(uuid), - created_at TIMESTAMPTZ NOT NULL, - PRIMARY KEY (token, user_uuid) -); diff --git a/migrations/2025-06-03-103311_remove_email_tokens/up.sql b/migrations/2025-06-03-103311_remove_email_tokens/up.sql deleted file mode 100644 index b41afe5..0000000 --- a/migrations/2025-06-03-103311_remove_email_tokens/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -DROP TABLE email_tokens; diff --git a/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql b/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql deleted file mode 100644 index 009d9e4..0000000 --- a/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql +++ /dev/null @@ -1,7 +0,0 @@ --- This file should undo anything in `up.sql` -CREATE TABLE password_reset_tokens ( - token VARCHAR(64) NOT NULL, - user_uuid uuid UNIQUE NOT NULL REFERENCES users(uuid), - created_at TIMESTAMPTZ NOT NULL, - PRIMARY KEY (token, user_uuid) -); diff --git a/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql b/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql deleted file mode 100644 index 181d7c5..0000000 --- a/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -DROP TABLE password_reset_tokens; diff --git a/migrations/2025-06-06-145916_guild_ownership_changes/down.sql b/migrations/2025-06-06-145916_guild_ownership_changes/down.sql deleted file mode 100644 index 21a08c9..0000000 --- a/migrations/2025-06-06-145916_guild_ownership_changes/down.sql +++ /dev/null @@ -1,14 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE guilds -ADD COLUMN owner_uuid UUID REFERENCES users(uuid); - -UPDATE guilds g -SET owner_uuid = gm.user_uuid -FROM guild_members gm -WHERE gm.guild_uuid = g.uuid AND gm.is_owner = TRUE; - -ALTER TABLE guilds -ALTER COLUMN owner_uuid SET NOT NULL; - -ALTER TABLE guild_members -DROP COLUMN is_owner; diff --git a/migrations/2025-06-06-145916_guild_ownership_changes/up.sql b/migrations/2025-06-06-145916_guild_ownership_changes/up.sql deleted file mode 100644 index b94323f..0000000 --- a/migrations/2025-06-06-145916_guild_ownership_changes/up.sql +++ /dev/null @@ -1,14 +0,0 @@ --- Your SQL goes here -ALTER TABLE guild_members -ADD COLUMN is_owner BOOLEAN NOT NULL DEFAULT false; - -UPDATE guild_members gm -SET is_owner = true -FROM guilds g -WHERE gm.guild_uuid = g.uuid AND gm.user_uuid = g.owner_uuid; - -CREATE UNIQUE INDEX one_owner_per_guild ON guild_members (guild_uuid) -WHERE is_owner; - -ALTER TABLE guilds -DROP COLUMN owner_uuid; diff --git a/migrations/2025-07-02-192220_change_maximum_url_lengths/down.sql b/migrations/2025-07-02-192220_change_maximum_url_lengths/down.sql deleted file mode 100644 index 69466b4..0000000 --- a/migrations/2025-07-02-192220_change_maximum_url_lengths/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE users ALTER COLUMN avatar TYPE varchar(100); -ALTER TABLE guilds ALTER COLUMN icon TYPE varchar(100); diff --git a/migrations/2025-07-02-192220_change_maximum_url_lengths/up.sql b/migrations/2025-07-02-192220_change_maximum_url_lengths/up.sql deleted file mode 100644 index bcc0afe..0000000 --- a/migrations/2025-07-02-192220_change_maximum_url_lengths/up.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Your SQL goes here -ALTER TABLE users ALTER COLUMN avatar TYPE varchar(8000); -ALTER TABLE guilds ALTER COLUMN icon TYPE varchar(8000); diff --git a/migrations/2025-07-04-183201_add_replies_to_messages/down.sql b/migrations/2025-07-04-183201_add_replies_to_messages/down.sql deleted file mode 100644 index bc705d7..0000000 --- a/migrations/2025-07-04-183201_add_replies_to_messages/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE messages DROP COLUMN reply_to; diff --git a/migrations/2025-07-04-183201_add_replies_to_messages/up.sql b/migrations/2025-07-04-183201_add_replies_to_messages/up.sql deleted file mode 100644 index ba90379..0000000 --- a/migrations/2025-07-04-183201_add_replies_to_messages/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE messages ADD COLUMN reply_to UUID REFERENCES messages(uuid) DEFAULT NULL; diff --git a/migrations/2025-07-07-131320_add_friends/down.sql b/migrations/2025-07-07-131320_add_friends/down.sql deleted file mode 100644 index 30637b7..0000000 --- a/migrations/2025-07-07-131320_add_friends/down.sql +++ /dev/null @@ -1,4 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE friend_requests; -DROP FUNCTION check_friend_request; -DROP TABLE friends; diff --git a/migrations/2025-07-07-131320_add_friends/up.sql b/migrations/2025-07-07-131320_add_friends/up.sql deleted file mode 100644 index 2ed45ab..0000000 --- a/migrations/2025-07-07-131320_add_friends/up.sql +++ /dev/null @@ -1,35 +0,0 @@ --- Your SQL goes here -CREATE TABLE friends ( - uuid1 UUID REFERENCES users(uuid), - uuid2 UUID REFERENCES users(uuid), - accepted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (uuid1, uuid2), - CHECK (uuid1 < uuid2) -); - -CREATE TABLE friend_requests ( - sender UUID REFERENCES users(uuid), - receiver UUID REFERENCES users(uuid), - requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (sender, receiver), - CHECK (sender <> receiver) -); - --- Create a function to check for existing friendships -CREATE FUNCTION check_friend_request() -RETURNS TRIGGER AS $$ -BEGIN - IF EXISTS ( - SELECT 1 FROM friends - WHERE (uuid1, uuid2) = (LEAST(NEW.sender, NEW.receiver), GREATEST(NEW.sender, NEW.receiver)) - ) THEN - RAISE EXCEPTION 'Users are already friends'; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -CREATE TRIGGER prevent_friend_request_conflict -BEFORE INSERT OR UPDATE ON friend_requests -FOR EACH ROW EXECUTE FUNCTION check_friend_request(); diff --git a/migrations/2025-07-13-155008_unique_guild_members/down.sql b/migrations/2025-07-13-155008_unique_guild_members/down.sql deleted file mode 100644 index 013105c..0000000 --- a/migrations/2025-07-13-155008_unique_guild_members/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE guild_members DROP CONSTRAINT guild_members_user_uuid_guild_uuid_key; diff --git a/migrations/2025-07-13-155008_unique_guild_members/up.sql b/migrations/2025-07-13-155008_unique_guild_members/up.sql deleted file mode 100644 index d139337..0000000 --- a/migrations/2025-07-13-155008_unique_guild_members/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE guild_members ADD UNIQUE (user_uuid, guild_uuid) \ No newline at end of file diff --git a/migrations/2025-07-15-002434_increase_device_name_length/down.sql b/migrations/2025-07-15-002434_increase_device_name_length/down.sql deleted file mode 100644 index 4fe6628..0000000 --- a/migrations/2025-07-15-002434_increase_device_name_length/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE refresh_tokens ALTER COLUMN device_name TYPE varchar(16); \ No newline at end of file diff --git a/migrations/2025-07-15-002434_increase_device_name_length/up.sql b/migrations/2025-07-15-002434_increase_device_name_length/up.sql deleted file mode 100644 index 9d44298..0000000 --- a/migrations/2025-07-15-002434_increase_device_name_length/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE refresh_tokens ALTER COLUMN device_name TYPE varchar(64); \ No newline at end of file diff --git a/migrations/2025-07-22-195121_add_ban/down.sql b/migrations/2025-07-22-195121_add_ban/down.sql deleted file mode 100644 index 62fe554..0000000 --- a/migrations/2025-07-22-195121_add_ban/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE guild_bans; diff --git a/migrations/2025-07-22-195121_add_ban/up.sql b/migrations/2025-07-22-195121_add_ban/up.sql deleted file mode 100644 index a590142..0000000 --- a/migrations/2025-07-22-195121_add_ban/up.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Your SQL goes here -CREATE TABLE guild_bans ( - guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, - user_uuid uuid NOT NULL REFERENCES users(uuid), - reason VARCHAR(200) DEFAULT NULL, - banned_since TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (user_uuid, guild_uuid) -); diff --git a/migrations/2025-07-31-133510_roles_uuid_index/down.sql b/migrations/2025-07-31-133510_roles_uuid_index/down.sql deleted file mode 100644 index efe3f3f..0000000 --- a/migrations/2025-07-31-133510_roles_uuid_index/down.sql +++ /dev/null @@ -1,5 +0,0 @@ --- This file should undo anything in `up.sql` -DROP INDEX roles_guuid_uuid; -ALTER TABLE roles DROP CONSTRAINT roles_pkey; -CREATE UNIQUE INDEX roles_pkey ON roles (uuid, guild_uuid); -ALTER TABLE roles ADD PRIMARY KEY USING INDEX roles_pkey; diff --git a/migrations/2025-07-31-133510_roles_uuid_index/up.sql b/migrations/2025-07-31-133510_roles_uuid_index/up.sql deleted file mode 100644 index 792e7fd..0000000 --- a/migrations/2025-07-31-133510_roles_uuid_index/up.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Your SQL goes here -ALTER TABLE roles DROP CONSTRAINT roles_pkey; -CREATE UNIQUE INDEX roles_pkey ON roles (uuid); -ALTER TABLE roles ADD PRIMARY KEY USING INDEX roles_pkey; -CREATE UNIQUE INDEX roles_guuid_uuid ON roles (uuid, guild_uuid); \ No newline at end of file diff --git a/migrations/2025-08-04-180235_add_status_to_user/down.sql b/migrations/2025-08-04-180235_add_status_to_user/down.sql deleted file mode 100644 index 163f7f1..0000000 --- a/migrations/2025-08-04-180235_add_status_to_user/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE users DROP COLUMN online_status; diff --git a/migrations/2025-08-04-180235_add_status_to_user/up.sql b/migrations/2025-08-04-180235_add_status_to_user/up.sql deleted file mode 100644 index ac16d77..0000000 --- a/migrations/2025-08-04-180235_add_status_to_user/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE users ADD COLUMN online_status INT2 NOT NULL DEFAULT 0; diff --git a/src/api/mod.rs b/src/api/mod.rs index 5aaa8a5..25391eb 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,16 +1,9 @@ -//! `/api` Contains the entire API - -use std::sync::Arc; - -use axum::{Router, routing::get}; - -use crate::AppState; +use actix_web::Scope; +use actix_web::web; mod v1; mod versions; -pub fn router(path: &str, app_state: Arc) -> Router> { - Router::new() - .route(&format!("{path}/versions"), get(versions::versions)) - .nest(&format!("{path}/v1"), v1::router(app_state)) +pub fn web() -> Scope { + web::scope("/api").service(v1::web()).service(versions::res) } diff --git a/src/api/v1/auth/devices.rs b/src/api/v1/auth/devices.rs deleted file mode 100644 index 35fe957..0000000 --- a/src/api/v1/auth/devices.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! `/api/v1/auth/devices` Returns list of logged in devices - -use std::sync::Arc; - -use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; -use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; -use diesel_async::RunQueryDsl; -use serde::Serialize; -use uuid::Uuid; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - schema::refresh_tokens::{self, dsl}, -}; - -#[derive(Serialize, Selectable, Queryable)] -#[diesel(table_name = refresh_tokens)] -#[diesel(check_for_backend(diesel::pg::Pg))] -struct Device { - device_name: String, - created_at: i64, -} - -/// `GET /api/v1/auth/devices` Returns list of logged in devices -/// -/// requires auth: no -/// -/// ### Response Example -/// ``` -/// json!([ -/// { -/// "device_name": "My Device!" -/// "created_at": "1752418856" -/// } -/// -/// ]); -/// ``` -pub async fn get( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let devices: Vec = dsl::refresh_tokens - .filter(dsl::uuid.eq(uuid)) - .select(Device::as_select()) - .get_results(&mut app_state.pool.get().await?) - .await?; - - Ok((StatusCode::OK, Json(devices))) -} diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 22cc838..c3e8bc7 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -1,62 +1,94 @@ -use std::{ - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; +use actix_web::{HttpResponse, post, web}; use argon2::{PasswordHash, PasswordVerifier}; -use axum::{ - Json, - extract::State, - http::{HeaderValue, StatusCode}, - response::IntoResponse, -}; use diesel::{ExpressionMethods, QueryDsl, dsl::insert_into}; use diesel_async::RunQueryDsl; use serde::Deserialize; +use uuid::Uuid; -use super::Response; use crate::{ - AppState, + Data, + api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, error::Error, schema::*, - utils::{ - PASSWORD_REGEX, generate_device_name, generate_token, new_refresh_token_cookie, - user_uuid_from_identifier, - }, + utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, }; +use super::Response; + #[derive(Deserialize)] -pub struct LoginInformation { +struct LoginInformation { username: String, password: String, + device_name: String, } +#[post("/login")] pub async fn response( - State(app_state): State>, - Json(login_information): Json, -) -> Result { + login_information: web::Json, + data: web::Data, +) -> Result { if !PASSWORD_REGEX.is_match(&login_information.password) { - return Err(Error::BadRequest("Bad password".to_string())); + return Ok(HttpResponse::Forbidden().json(r#"{ "password_hashed": false }"#)); } use users::dsl; - let mut conn = app_state.pool.get().await?; + let mut conn = data.pool.get().await?; - let uuid = user_uuid_from_identifier(&mut conn, &login_information.username).await?; + if EMAIL_REGEX.is_match(&login_information.username) { + // FIXME: error handling, right now i just want this to work + let (uuid, password): (Uuid, String) = dsl::users + .filter(dsl::email.eq(&login_information.username)) + .select((dsl::uuid, dsl::password)) + .get_result(&mut conn) + .await?; - let database_password: String = dsl::users - .filter(dsl::uuid.eq(uuid)) - .select(dsl::password) - .get_result(&mut conn) - .await?; + return login( + data.clone(), + uuid, + login_information.password.clone(), + password, + login_information.device_name.clone(), + ) + .await; + } else if USERNAME_REGEX.is_match(&login_information.username) { + // FIXME: error handling, right now i just want this to work + let (uuid, password): (Uuid, String) = dsl::users + .filter(dsl::username.eq(&login_information.username)) + .select((dsl::uuid, dsl::password)) + .get_result(&mut conn) + .await?; + + return login( + data.clone(), + uuid, + login_information.password.clone(), + password, + login_information.device_name.clone(), + ) + .await; + } + + Ok(HttpResponse::Unauthorized().finish()) +} + +async fn login( + data: actix_web::web::Data, + uuid: Uuid, + request_password: String, + database_password: String, + device_name: String, +) -> Result { + let mut conn = data.pool.get().await?; let parsed_hash = PasswordHash::new(&database_password) .map_err(|e| Error::PasswordHashError(e.to_string()))?; - if app_state + if data .argon2 - .verify_password(login_information.password.as_bytes(), &parsed_hash) + .verify_password(request_password.as_bytes(), &parsed_hash) .is_err() { return Err(Error::Unauthorized( @@ -64,21 +96,19 @@ pub async fn response( )); } - let refresh_token = generate_token::<32>()?; - let access_token = generate_token::<16>()?; + let refresh_token = generate_refresh_token()?; + let access_token = generate_access_token()?; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; use refresh_tokens::dsl as rdsl; - let device_name = generate_device_name(); - insert_into(refresh_tokens::table) .values(( rdsl::token.eq(&refresh_token), rdsl::uuid.eq(uuid), rdsl::created_at.eq(current_time), - rdsl::device_name.eq(&device_name), + rdsl::device_name.eq(device_name), )) .execute(&mut conn) .await?; @@ -95,21 +125,7 @@ pub async fn response( .execute(&mut conn) .await?; - let mut response = ( - StatusCode::OK, - Json(Response { - access_token, - device_name, - }), - ) - .into_response(); - - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str( - &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), - )?, - ); - - Ok(response) + Ok(HttpResponse::Ok() + .cookie(refresh_token_cookie(refresh_token)) + .json(Response { access_token })) } diff --git a/src/api/v1/auth/logout.rs b/src/api/v1/auth/logout.rs deleted file mode 100644 index 977d452..0000000 --- a/src/api/v1/auth/logout.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::sync::Arc; - -use axum::{ - extract::State, - http::{HeaderValue, StatusCode}, - response::IntoResponse, -}; -use axum_extra::extract::CookieJar; -use diesel::{ExpressionMethods, delete}; -use diesel_async::RunQueryDsl; - -use crate::{ - AppState, - error::Error, - schema::refresh_tokens::{self, dsl}, -}; - -/// `GET /api/v1/logout` -/// -/// requires auth: kinda, needs refresh token set but no access token is technically required -/// -/// ### Responses -/// -/// 200 Logged out -/// -/// 404 Refresh token is invalid -/// -/// 401 Unauthorized (no refresh token found) -/// -pub async fn res( - State(app_state): State>, - jar: CookieJar, -) -> Result { - let mut refresh_token_cookie = jar - .get("refresh_token") - .ok_or(Error::Unauthorized( - "request has no refresh token".to_string(), - ))? - .to_owned(); - - let refresh_token = String::from(refresh_token_cookie.value_trimmed()); - - let mut conn = app_state.pool.get().await?; - - let deleted = delete(refresh_tokens::table) - .filter(dsl::token.eq(refresh_token)) - .execute(&mut conn) - .await?; - - let mut response; - - if deleted == 0 { - response = StatusCode::NOT_FOUND.into_response(); - } else { - response = StatusCode::OK.into_response(); - } - - refresh_token_cookie.make_removal(); - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str(&refresh_token_cookie.to_string())?, - ); - - Ok(response) -} diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 9a72f11..2bc0d0b 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -1,57 +1,42 @@ use std::{ - sync::Arc, + sync::LazyLock, time::{SystemTime, UNIX_EPOCH}, }; -use axum::{ - Router, - extract::{Request, State}, - middleware::{Next, from_fn_with_state}, - response::IntoResponse, - routing::{delete, get, post}, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, -}; +use actix_web::{Scope, web}; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; +use regex::Regex; use serde::Serialize; use uuid::Uuid; -use crate::{AppState, Conn, error::Error, schema::access_tokens::dsl}; +use crate::{Conn, error::Error, schema::access_tokens::dsl}; -mod devices; mod login; -mod logout; mod refresh; mod register; -mod reset_password; mod revoke; -mod verify_email; #[derive(Serialize)] -pub struct Response { +struct Response { access_token: String, - device_name: String, } -pub fn router(app_state: Arc) -> Router> { - let router_with_auth = Router::new() - .route("/verify-email", get(verify_email::get)) - .route("/verify-email", post(verify_email::post)) - .route("/revoke", post(revoke::post)) - .route("/devices", get(devices::get)) - .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer)); +static EMAIL_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+(?:\.[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+)*@(?:[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?").unwrap() +}); - Router::new() - .route("/register", post(register::post)) - .route("/login", post(login::response)) - .route("/logout", delete(logout::res)) - .route("/refresh", post(refresh::post)) - .route("/reset-password", get(reset_password::get)) - .route("/reset-password", post(reset_password::post)) - .merge(router_with_auth) +static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); + +// Password is expected to be hashed using SHA3-384 +static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); + +pub fn web() -> Scope { + web::scope("/auth") + .service(register::res) + .service(login::response) + .service(refresh::res) + .service(revoke::res) } pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result { @@ -78,21 +63,3 @@ pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result(pub Uuid); - -impl CurrentUser { - pub async fn check_auth_layer( - State(app_state): State>, - TypedHeader(auth): TypedHeader>, - mut req: Request, - next: Next, - ) -> Result { - let current_user = - CurrentUser(check_access_token(auth.token(), &mut app_state.pool.get().await?).await?); - - req.extensions_mut().insert(current_user); - Ok(next.run(req).await) - } -} diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index 4b96226..303748a 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -1,45 +1,34 @@ -use axum::{ - Json, - extract::State, - http::{HeaderValue, StatusCode}, - response::IntoResponse, -}; -use axum_extra::extract::CookieJar; +use actix_web::{HttpRequest, HttpResponse, post, web}; use diesel::{ExpressionMethods, QueryDsl, delete, update}; use diesel_async::RunQueryDsl; use log::error; -use std::{ - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; -use super::Response; use crate::{ - AppState, + Data, error::Error, schema::{ access_tokens::{self, dsl}, refresh_tokens::{self, dsl as rdsl}, }, - utils::{generate_token, new_refresh_token_cookie}, + utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, }; -pub async fn post( - State(app_state): State>, - jar: CookieJar, -) -> Result { - let mut refresh_token_cookie = jar - .get("refresh_token") - .ok_or(Error::Unauthorized( - "request has no refresh token".to_string(), - ))? - .to_owned(); +use super::Response; - let mut refresh_token = String::from(refresh_token_cookie.value_trimmed()); +#[post("/refresh")] +pub async fn res(req: HttpRequest, data: web::Data) -> Result { + let recv_refresh_token_cookie = req.cookie("refresh_token"); + + if recv_refresh_token_cookie.is_none() { + return Ok(HttpResponse::Unauthorized().finish()); + } + + let mut refresh_token = String::from(recv_refresh_token_cookie.unwrap().value()); let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; - let mut conn = app_state.pool.get().await?; + let mut conn = data.pool.get().await?; if let Ok(created_at) = rdsl::refresh_tokens .filter(rdsl::token.eq(&refresh_token)) @@ -55,25 +44,29 @@ pub async fn post( .execute(&mut conn) .await { - error!("{error}"); + error!("{}", error); } - let mut response = StatusCode::UNAUTHORIZED.into_response(); + let mut refresh_token_cookie = refresh_token_cookie(refresh_token); refresh_token_cookie.make_removal(); - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str(&refresh_token_cookie.to_string())?, - ); - return Ok(response); + return Ok(HttpResponse::Unauthorized() + .cookie(refresh_token_cookie) + .finish()); } let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; - let mut device_name: String = String::new(); if lifetime > 1987200 { - let new_refresh_token = generate_token::<32>()?; + let new_refresh_token = generate_refresh_token(); + + if new_refresh_token.is_err() { + error!("{}", new_refresh_token.unwrap_err()); + return Ok(HttpResponse::InternalServerError().finish()); + } + + let new_refresh_token = new_refresh_token.unwrap(); match update(refresh_tokens::table) .filter(rdsl::token.eq(&refresh_token)) @@ -81,21 +74,19 @@ pub async fn post( rdsl::token.eq(&new_refresh_token), rdsl::created_at.eq(current_time), )) - .returning(rdsl::device_name) - .get_result::(&mut conn) + .execute(&mut conn) .await { - Ok(existing_device_name) => { + Ok(_) => { refresh_token = new_refresh_token; - device_name = existing_device_name; } Err(error) => { - error!("{error}"); + error!("{}", error); } } } - let access_token = generate_token::<16>()?; + let access_token = generate_access_token()?; update(access_tokens::table) .filter(dsl::refresh_token.eq(&refresh_token)) @@ -106,33 +97,16 @@ pub async fn post( .execute(&mut conn) .await?; - let mut response = ( - StatusCode::OK, - Json(Response { - access_token, - device_name, - }), - ) - .into_response(); - - // TODO: Dont set this when refresh token is unchanged - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str( - &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), - )?, - ); - - return Ok(response); + return Ok(HttpResponse::Ok() + .cookie(refresh_token_cookie(refresh_token)) + .json(Response { access_token })); } - let mut response = StatusCode::UNAUTHORIZED.into_response(); + let mut refresh_token_cookie = refresh_token_cookie(refresh_token); refresh_token_cookie.make_removal(); - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str(&refresh_token_cookie.to_string())?, - ); - Ok(response) + Ok(HttpResponse::Unauthorized() + .cookie(refresh_token_cookie) + .finish()) } diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 545e5aa..75aeb9d 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -1,18 +1,10 @@ -use std::{ - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; +use actix_web::{HttpResponse, post, web}; use argon2::{ PasswordHasher, password_hash::{SaltString, rand_core::OsRng}, }; -use axum::{ - Json, - extract::State, - http::{HeaderValue, StatusCode}, - response::IntoResponse, -}; use diesel::{ExpressionMethods, dsl::insert_into}; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; @@ -20,35 +12,37 @@ use uuid::Uuid; use super::Response; use crate::{ - AppState, + Data, + api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, error::Error, - objects::Member, schema::{ access_tokens::{self, dsl as adsl}, refresh_tokens::{self, dsl as rdsl}, users::{self, dsl as udsl}, }, - utils::{ - EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_device_name, generate_token, - new_refresh_token_cookie, - }, + utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, }; #[derive(Deserialize)] -pub struct AccountInformation { +struct AccountInformation { identifier: String, email: String, password: String, + device_name: String, } #[derive(Serialize)] -pub struct ResponseError { +struct ResponseError { signups_enabled: bool, gorb_id_valid: bool, gorb_id_available: bool, email_valid: bool, email_available: bool, - password_strength: bool, + password_hashed: bool, + password_minimum_length: bool, + password_special_characters: bool, + password_letters: bool, + password_numbers: bool, } impl Default for ResponseError { @@ -59,66 +53,53 @@ impl Default for ResponseError { gorb_id_available: true, email_valid: true, email_available: true, - password_strength: true, + password_hashed: true, + password_minimum_length: true, + password_special_characters: true, + password_letters: true, + password_numbers: true, } } } -pub async fn post( - State(app_state): State>, - Json(account_information): Json, -) -> Result { - if !app_state.config.instance.registration { - return Err(Error::Forbidden( - "registration is disabled on this instance".to_string(), - )); - } - +#[post("/register")] +pub async fn res( + account_information: web::Json, + data: web::Data, +) -> Result { let uuid = Uuid::now_v7(); if !EMAIL_REGEX.is_match(&account_information.email) { - return Ok(( - StatusCode::FORBIDDEN, - Json(ResponseError { - email_valid: false, - ..Default::default() - }), - ) - .into_response()); + return Ok(HttpResponse::Forbidden().json(ResponseError { + email_valid: false, + ..Default::default() + })); } if !USERNAME_REGEX.is_match(&account_information.identifier) || account_information.identifier.len() < 3 || account_information.identifier.len() > 32 { - return Ok(( - StatusCode::FORBIDDEN, - Json(ResponseError { - gorb_id_valid: false, - ..Default::default() - }), - ) - .into_response()); + return Ok(HttpResponse::Forbidden().json(ResponseError { + gorb_id_valid: false, + ..Default::default() + })); } if !PASSWORD_REGEX.is_match(&account_information.password) { - return Ok(( - StatusCode::FORBIDDEN, - Json(ResponseError { - password_strength: false, - ..Default::default() - }), - ) - .into_response()); + return Ok(HttpResponse::Forbidden().json(ResponseError { + password_hashed: false, + ..Default::default() + })); } let salt = SaltString::generate(&mut OsRng); - if let Ok(hashed_password) = app_state + if let Ok(hashed_password) = data .argon2 .hash_password(account_information.password.as_bytes(), &salt) { - let mut conn = app_state.pool.get().await?; + let mut conn = data.pool.get().await?; // TODO: Check security of this implementation insert_into(users::table) @@ -131,19 +112,17 @@ pub async fn post( .execute(&mut conn) .await?; - let refresh_token = generate_token::<32>()?; - let access_token = generate_token::<16>()?; + let refresh_token = generate_refresh_token()?; + let access_token = generate_access_token()?; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; - let device_name = generate_device_name(); - insert_into(refresh_tokens::table) .values(( rdsl::token.eq(&refresh_token), rdsl::uuid.eq(uuid), rdsl::created_at.eq(current_time), - rdsl::device_name.eq(&device_name), + rdsl::device_name.eq(&account_information.device_name), )) .execute(&mut conn) .await?; @@ -158,28 +137,10 @@ pub async fn post( .execute(&mut conn) .await?; - if let Some(initial_guild) = app_state.config.instance.initial_guild { - Member::new(&mut conn, &app_state.cache_pool, uuid, initial_guild).await?; - } - - let mut response = ( - StatusCode::OK, - Json(Response { - access_token, - device_name, - }), - ) - .into_response(); - - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str( - &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), - )?, - ); - - return Ok(response); + return Ok(HttpResponse::Ok() + .cookie(refresh_token_cookie(refresh_token)) + .json(Response { access_token })); } - Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) + Ok(HttpResponse::InternalServerError().finish()) } diff --git a/src/api/v1/auth/reset_password.rs b/src/api/v1/auth/reset_password.rs deleted file mode 100644 index 35c4b41..0000000 --- a/src/api/v1/auth/reset_password.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! `/api/v1/auth/reset-password` Endpoints for resetting user password - -use std::sync::Arc; - -use axum::{ - Json, - extract::{Query, State}, - http::StatusCode, - response::IntoResponse, -}; -use chrono::{Duration, Utc}; -use serde::Deserialize; - -use crate::{AppState, error::Error, objects::PasswordResetToken}; - -#[derive(Deserialize)] -pub struct QueryParams { - identifier: String, -} - -/// `GET /api/v1/auth/reset-password` Sends password reset email to user -/// -/// requires auth? no -/// -/// ### Query Parameters -/// identifier: Email or username -/// -/// ### Responses -/// 200 Email sent -/// -/// 429 Too Many Requests -/// -/// 404 Not found -/// -/// 400 Bad request -/// -pub async fn get( - State(app_state): State>, - query: Query, -) -> Result { - let mut conn = app_state.pool.get().await?; - - if let Ok(password_reset_token) = PasswordResetToken::get_with_identifier( - &mut conn, - &app_state.cache_pool, - query.identifier.clone(), - ) - .await - { - if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { - password_reset_token.delete(&app_state.cache_pool).await?; - } else { - return Err(Error::TooManyRequests( - "Please allow 1 hour before sending a new email".to_string(), - )); - } - } - - PasswordResetToken::new(&mut conn, &app_state, query.identifier.clone()).await?; - - Ok(StatusCode::OK) -} - -#[derive(Deserialize)] -pub struct ResetPassword { - password: String, - token: String, -} - -/// `POST /api/v1/auth/reset-password` Resets user password -/// -/// requires auth? no -/// -/// ### Request Example: -/// ``` -/// json!({ -/// "password": "1608c17a27f6ae3891c23d680c73ae91528f20a54dcf4973e2c3126b9734f48b7253047f2395b51bb8a44a6daa188003", -/// "token": "a3f7e29c1b8d0456e2c9f83b7a1d6e4f5028c3b9a7e1f2d5c6b8a0d3e7f4a2b" -/// }); -/// ``` -/// -/// ### Responses -/// 200 Success -/// -/// 410 Token Expired -/// -/// 404 Not Found -/// -/// 400 Bad Request -/// -pub async fn post( - State(app_state): State>, - reset_password: Json, -) -> Result { - let password_reset_token = - PasswordResetToken::get(&app_state.cache_pool, reset_password.token.clone()).await?; - - password_reset_token - .set_password( - &mut app_state.pool.get().await?, - &app_state, - reset_password.password.clone(), - ) - .await?; - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/auth/revoke.rs b/src/api/v1/auth/revoke.rs index 90b96ae..2e95884 100644 --- a/src/api/v1/auth/revoke.rs +++ b/src/api/v1/auth/revoke.rs @@ -1,35 +1,38 @@ -use std::sync::Arc; - +use actix_web::{HttpRequest, HttpResponse, post, web}; use argon2::{PasswordHash, PasswordVerifier}; -use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; use diesel::{ExpressionMethods, QueryDsl, delete}; use diesel_async::RunQueryDsl; use serde::Deserialize; -use uuid::Uuid; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, - schema::{ - refresh_tokens::{self, dsl as rdsl}, - users::dsl as udsl, - }, + schema::refresh_tokens::{self, dsl as rdsl}, + schema::users::dsl as udsl, + utils::get_auth_header, }; #[derive(Deserialize)] -pub struct RevokeRequest { +struct RevokeRequest { password: String, device_name: String, } // TODO: Should maybe be a delete request? -pub async fn post( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, - Json(revoke_request): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; +#[post("/revoke")] +pub async fn res( + req: HttpRequest, + revoke_request: web::Json, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; let database_password: String = udsl::users .filter(udsl::uuid.eq(uuid)) @@ -40,7 +43,7 @@ pub async fn post( let hashed_password = PasswordHash::new(&database_password) .map_err(|e| Error::PasswordHashError(e.to_string()))?; - if app_state + if data .argon2 .verify_password(revoke_request.password.as_bytes(), &hashed_password) .is_err() @@ -56,5 +59,5 @@ pub async fn post( .execute(&mut conn) .await?; - Ok(StatusCode::OK) + Ok(HttpResponse::Ok().finish()) } diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs deleted file mode 100644 index 1cb8aef..0000000 --- a/src/api/v1/auth/verify_email.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! `/api/v1/auth/verify-email` Endpoints for verifying user emails - -use std::sync::Arc; - -use axum::{ - Extension, - extract::{Query, State}, - http::StatusCode, - response::IntoResponse, -}; -use chrono::{Duration, Utc}; -use serde::Deserialize; -use uuid::Uuid; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{EmailToken, Me}, -}; - -#[derive(Deserialize)] -pub struct QueryParams { - token: String, -} - -/// `GET /api/v1/auth/verify-email` Verifies user email address -/// -/// requires auth? yes -/// -/// ### Query Parameters -/// token -/// -/// ### Responses -/// 200 Success -/// -/// 204 Already verified -/// -/// 410 Token Expired -/// -/// 404 Not Found -/// -/// 401 Unauthorized -/// -pub async fn get( - State(app_state): State>, - Query(query): Query, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - let me = Me::get(&mut conn, uuid).await?; - - if me.email_verified { - return Ok(StatusCode::NO_CONTENT); - } - - let email_token = EmailToken::get(&app_state.cache_pool, me.uuid).await?; - - if query.token != email_token.token { - return Ok(StatusCode::UNAUTHORIZED); - } - - me.verify_email(&mut conn).await?; - - email_token.delete(&app_state.cache_pool).await?; - - Ok(StatusCode::OK) -} - -/// `POST /api/v1/auth/verify-email` Sends user verification email -/// -/// requires auth? yes -/// -/// ### Responses -/// 200 Email sent -/// -/// 204 Already verified -/// -/// 429 Too Many Requests -/// -/// 401 Unauthorized -/// -pub async fn post( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; - - if me.email_verified { - return Ok(StatusCode::NO_CONTENT); - } - - if let Ok(email_token) = EmailToken::get(&app_state.cache_pool, me.uuid).await { - if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(1) { - email_token.delete(&app_state.cache_pool).await?; - } else { - return Err(Error::TooManyRequests( - "Please allow 1 hour before sending a new email".to_string(), - )); - } - } - - EmailToken::new(&app_state, me).await?; - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/channels/mod.rs b/src/api/v1/channels/mod.rs deleted file mode 100644 index fa90ccd..0000000 --- a/src/api/v1/channels/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::sync::Arc; - -use axum::{ - Router, - middleware::from_fn_with_state, - routing::{any, delete, get, patch}, -}; -//use socketioxide::SocketIo; - -use crate::{AppState, api::v1::auth::CurrentUser}; - -mod uuid; - -pub fn router(app_state: Arc) -> Router> { - let router_with_auth = Router::new() - .route("/{uuid}", get(uuid::get)) - .route("/{uuid}", delete(uuid::delete)) - .route("/{uuid}", patch(uuid::patch)) - .route("/{uuid}/messages", get(uuid::messages::get)) - .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer)); - - Router::new() - .route("/{uuid}/socket", any(uuid::socket::ws)) - .merge(router_with_auth) -} diff --git a/src/api/v1/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs deleted file mode 100644 index 1f9010d..0000000 --- a/src/api/v1/channels/uuid/messages.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! `/api/v1/channels/{uuid}/messages` Endpoints related to channel messages - -use std::sync::Arc; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Channel, Member}, - utils::global_checks, -}; -use ::uuid::Uuid; -use axum::{ - Extension, Json, - extract::{Path, Query, State}, - http::StatusCode, - response::IntoResponse, -}; -use serde::Deserialize; - -#[derive(Deserialize)] -pub struct MessageRequest { - amount: i64, - offset: i64, -} - -/// `GET /api/v1/channels/{uuid}/messages` Returns user with the given UUID -/// -/// requires auth: yes -/// -/// requires relation: yes -/// -/// ### Request Example -/// ``` -/// json!({ -/// "amount": 100, -/// "offset": 0 -/// }) -/// ``` -/// -/// ### Response Example -/// ``` -/// json!({ -/// "uuid": "01971976-8618-74c0-b040-7ffbc44823f6", -/// "channel_uuid": "0196fcb1-e886-7de3-b685-0ee46def9a7b", -/// "user_uuid": "0196fc96-a822-76b0-b9bf-a9de232f54b7", -/// "message": "test", -/// "user": { -/// "uuid": "0196fc96-a822-76b0-b9bf-a9de232f54b7", -/// "username": "1234", -/// "display_name": null, -/// "avatar": "https://cdn.gorb.app/avatar/0196fc96-a822-76b0-b9bf-a9de232f54b7/avatar.jpg" -/// } -/// }); -/// ``` -/// -pub async fn get( - State(app_state): State>, - Path(channel_uuid): Path, - Query(message_request): Query, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; - - Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - - let messages = channel - .fetch_messages( - &mut conn, - &app_state.cache_pool, - message_request.amount, - message_request.offset, - ) - .await?; - - Ok((StatusCode::OK, Json(messages))) -} diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs deleted file mode 100644 index f5566b3..0000000 --- a/src/api/v1/channels/uuid/mod.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! `/api/v1/channels/{uuid}` Channel specific endpoints - -pub mod messages; -pub mod socket; - -use std::sync::Arc; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Channel, Member, Permissions}, - utils::global_checks, -}; -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; - -use serde::Deserialize; -use uuid::Uuid; - -pub async fn get( - State(app_state): State>, - Path(channel_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; - - Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - - Ok((StatusCode::OK, Json(channel))) -} - -pub async fn delete( - State(app_state): State>, - Path(channel_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; - - let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - - member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel) - .await?; - - channel.delete(&mut conn, &app_state.cache_pool).await?; - - Ok(StatusCode::OK) -} - -#[derive(Deserialize)] -pub struct NewInfo { - name: Option, - description: Option, - is_above: Option, -} - -/// `PATCH /api/v1/channels/{uuid}` Returns user with the given UUID -/// -/// requires auth: yes -/// -/// requires relation: yes -/// -/// ### Request Example -/// All fields are optional and can be nulled/dropped if only changing 1 value -/// ``` -/// json!({ -/// "name": "gaming-chat", -/// "description": "Gaming related topics.", -/// "is_above": "398f6d7b-752c-4348-9771-fe6024adbfb1" -/// }); -/// ``` -/// -/// ### Response Example -/// ``` -/// json!({ -/// uuid: "cdcac171-5add-4f88-9559-3a247c8bba2c", -/// guild_uuid: "383d2afa-082f-4dd3-9050-ca6ed91487b6", -/// name: "gaming-chat", -/// description: "Gaming related topics.", -/// is_above: "398f6d7b-752c-4348-9771-fe6024adbfb1", -/// permissions: { -/// role_uuid: "79cc0806-0f37-4a06-a468-6639c4311a2d", -/// permissions: 0 -/// } -/// }); -/// ``` -/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -pub async fn patch( - State(app_state): State>, - Path(channel_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - Json(new_info): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let mut channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; - - let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - - member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel) - .await?; - - if let Some(new_name) = &new_info.name { - channel - .set_name(&mut conn, &app_state.cache_pool, new_name.to_string()) - .await?; - } - - if let Some(new_description) = &new_info.description { - channel - .set_description( - &mut conn, - &app_state.cache_pool, - new_description.to_string(), - ) - .await?; - } - - if let Some(new_is_above) = &new_info.is_above { - channel - .set_description(&mut conn, &app_state.cache_pool, new_is_above.to_string()) - .await?; - } - - Ok((StatusCode::OK, Json(channel))) -} diff --git a/src/api/v1/channels/uuid/socket.rs b/src/api/v1/channels/uuid/socket.rs deleted file mode 100644 index ac04301..0000000 --- a/src/api/v1/channels/uuid/socket.rs +++ /dev/null @@ -1,139 +0,0 @@ -use std::sync::Arc; - -use axum::{ - extract::{Path, State, WebSocketUpgrade, ws::Message}, - http::HeaderMap, - response::IntoResponse, -}; -use futures_util::{SinkExt, StreamExt}; -use serde::Deserialize; -use uuid::Uuid; - -use crate::{ - AppState, - api::v1::auth::check_access_token, - error::Error, - objects::{Channel, Member}, - utils::global_checks, -}; - -#[derive(Deserialize)] -struct MessageBody { - message: String, - reply_to: Option, -} - -pub async fn ws( - ws: WebSocketUpgrade, - State(app_state): State>, - Path(channel_uuid): Path, - headers: HeaderMap, -) -> Result { - // Retrieve auth header - let auth_token = headers.get(axum::http::header::SEC_WEBSOCKET_PROTOCOL); - - if auth_token.is_none() { - return Err(Error::Unauthorized( - "No authorization header provided".to_string(), - )); - } - - let auth_raw = auth_token.unwrap().to_str()?; - - let mut auth = auth_raw.split_whitespace(); - - let response_proto = auth.next(); - - let auth_value = auth.next(); - - if response_proto.is_none() { - return Err(Error::BadRequest( - "Sec-WebSocket-Protocol header is empty".to_string(), - )); - } else if response_proto.is_some_and(|rp| rp != "Authorization,") { - return Err(Error::BadRequest( - "First protocol should be Authorization".to_string(), - )); - } - - if auth_value.is_none() { - return Err(Error::BadRequest("No token provided".to_string())); - } - - let auth_header = auth_value.unwrap(); - - let mut conn = app_state - .pool - .get() - .await - .map_err(crate::error::Error::from)?; - - // Authorize client using auth header - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; - - Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - - let mut pubsub = app_state - .cache_pool - .get_async_pubsub() - .await - .map_err(crate::error::Error::from)?; - - let mut res = ws.on_upgrade(async move |socket| { - let (mut sender, mut receiver) = socket.split(); - - tokio::spawn(async move { - pubsub.subscribe(channel_uuid.to_string()).await?; - while let Some(msg) = pubsub.on_message().next().await { - let payload: String = msg.get_payload()?; - sender.send(payload.into()).await?; - } - - Ok::<(), crate::error::Error>(()) - }); - - tokio::spawn(async move { - while let Some(msg) = receiver.next().await { - if let Ok(Message::Text(text)) = msg { - let message_body: MessageBody = serde_json::from_str(&text)?; - - let message = channel - .new_message( - &mut conn, - &app_state.cache_pool, - uuid, - message_body.message, - message_body.reply_to, - ) - .await?; - - redis::cmd("PUBLISH") - .arg(&[channel_uuid.to_string(), serde_json::to_string(&message)?]) - .exec_async( - &mut app_state - .cache_pool - .get_multiplexed_tokio_connection() - .await?, - ) - .await?; - } - } - - Ok::<(), crate::error::Error>(()) - }); - }); - - let headers = res.headers_mut(); - - headers.append( - axum::http::header::SEC_WEBSOCKET_PROTOCOL, - "Authorization".parse()?, - ); - - // respond immediately with response connected to WS session - Ok(res) -} diff --git a/src/api/v1/guilds/mod.rs b/src/api/v1/guilds/mod.rs deleted file mode 100644 index 5b9f089..0000000 --- a/src/api/v1/guilds/mod.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! `/api/v1/guilds` Guild related endpoints - -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{ - Extension, Json, Router, - extract::State, - http::StatusCode, - response::IntoResponse, - routing::{get, post}, -}; -use serde::Deserialize; - -mod uuid; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Guild, StartAmountQuery}, - utils::global_checks, -}; - -#[derive(Deserialize)] -pub struct GuildInfo { - name: String, -} - -pub fn router() -> Router> { - Router::new() - .route("/", post(new)) - .route("/", get(get_guilds)) - .nest("/{uuid}", uuid::router()) -} - -/// `POST /api/v1/guilds` Creates a new guild -/// -/// requires auth: yes -/// -/// ### Request Example -/// ``` -/// json!({ -/// "name": "My new server!" -/// }); -/// ``` -/// -/// ### Response Example -/// ``` -/// json!({ -/// "uuid": "383d2afa-082f-4dd3-9050-ca6ed91487b6", -/// "name": "My new server!", -/// "description": null, -/// "icon": null, -/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", -/// "roles": [], -/// "member_count": 1 -/// }); -/// ``` -/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -pub async fn new( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, - Json(guild_info): Json, -) -> Result { - let guild = Guild::new( - &mut app_state.pool.get().await?, - guild_info.name.clone(), - uuid, - ) - .await?; - - Ok((StatusCode::OK, Json(guild))) -} - -/// `GET /api/v1/servers` Fetches all guilds -/// -/// requires auth: yes -/// -/// requires admin: yes -/// -/// ### Response Example -/// ``` -/// json!([ -/// { -/// "uuid": "383d2afa-082f-4dd3-9050-ca6ed91487b6", -/// "name": "My new server!", -/// "description": null, -/// "icon": null, -/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", -/// "roles": [], -/// "member_count": 1 -/// }, -/// { -/// "uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", -/// "name": "My first server!", -/// "description": "This is a cool and nullable description!", -/// "icon": "https://nullable-url/path/to/icon.png", -/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", -/// "roles": [ -/// { -/// "uuid": "be0e4da4-cf73-4f45-98f8-bb1c73d1ab8b", -/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", -/// "name": "Cool people", -/// "color": 15650773, -/// "is_above": c7432f1c-f4ad-4ad3-8216-51388b6abb5b, -/// "permissions": 0 -/// } -/// { -/// "uuid": "c7432f1c-f4ad-4ad3-8216-51388b6abb5b", -/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", -/// "name": "Equally cool people", -/// "color": 16777215, -/// "is_above": null, -/// "permissions": 0 -/// } -/// ], -/// "member_count": 20 -/// } -/// ]); -/// ``` -/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -pub async fn get_guilds( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, - Json(request_query): Json, -) -> Result { - let start = request_query.start.unwrap_or(0); - let amount = request_query.amount.unwrap_or(10); - - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let guilds = Guild::fetch_amount(&mut conn, start, amount).await?; - - Ok((StatusCode::OK, Json(guilds))) -} diff --git a/src/api/v1/guilds/uuid/bans.rs b/src/api/v1/guilds/uuid/bans.rs deleted file mode 100644 index 2e31a59..0000000 --- a/src/api/v1/guilds/uuid/bans.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::sync::Arc; - -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use uuid::Uuid; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{GuildBan, Member, Permissions}, - utils::global_checks, -}; - -pub async fn get( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let caller = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - caller - .check_permission(&mut conn, &app_state.cache_pool, Permissions::BanMember) - .await?; - - let all_guild_bans = GuildBan::fetch_all(&mut conn, guild_uuid).await?; - - Ok((StatusCode::OK, Json(all_guild_bans))) -} - -pub async fn unban( - State(app_state): State>, - Path((guild_uuid, user_uuid)): Path<(Uuid, Uuid)>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let caller = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - caller - .check_permission(&mut conn, &app_state.cache_pool, Permissions::BanMember) - .await?; - - let ban = GuildBan::fetch_one(&mut conn, guild_uuid, user_uuid).await?; - - ban.unban(&mut conn).await?; - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs deleted file mode 100644 index 1cd7f78..0000000 --- a/src/api/v1/guilds/uuid/channels.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use serde::Deserialize; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Channel, Member, Permissions}, - utils::{CacheFns, global_checks, order_by_is_above}, -}; - -#[derive(Deserialize)] -pub struct ChannelInfo { - name: String, - description: Option, -} - -pub async fn get( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - if let Ok(cache_hit) = app_state - .cache_pool - .get_cache_key::>(format!("{guild_uuid}_channels")) - .await - { - return Ok((StatusCode::OK, Json(cache_hit)).into_response()); - } - - let channels = Channel::fetch_all(&mut conn, guild_uuid).await?; - - let channels_ordered = order_by_is_above(channels).await?; - - app_state - .cache_pool - .set_cache_key( - format!("{guild_uuid}_channels"), - channels_ordered.clone(), - 1800, - ) - .await?; - - Ok((StatusCode::OK, Json(channels_ordered)).into_response()) -} - -pub async fn create( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - Json(channel_info): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel) - .await?; - - let channel = Channel::new( - &mut conn, - &app_state.cache_pool, - guild_uuid, - channel_info.name.clone(), - channel_info.description.clone(), - ) - .await?; - - Ok((StatusCode::OK, Json(channel))) -} diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs deleted file mode 100644 index fa06f44..0000000 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::sync::Arc; - -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use serde::Deserialize; -use uuid::Uuid; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Guild, Member, Permissions}, - utils::global_checks, -}; - -#[derive(Deserialize)] -pub struct InviteRequest { - custom_id: Option, -} - -pub async fn get( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; - - let invites = guild.get_invites(&mut conn).await?; - - Ok((StatusCode::OK, Json(invites))) -} - -pub async fn create( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - Json(invite_request): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::CreateInvite) - .await?; - - let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; - - let invite = guild - .create_invite(&mut conn, uuid, invite_request.custom_id.clone()) - .await?; - - Ok((StatusCode::OK, Json(invite))) -} diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs deleted file mode 100644 index 0e1d2bc..0000000 --- a/src/api/v1/guilds/uuid/members.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{ - Extension, Json, - extract::{Path, Query, State}, - http::StatusCode, - response::IntoResponse, -}; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Me, Member, PaginationRequest}, - utils::global_checks, -}; - -pub async fn get( - State(app_state): State>, - Path(guild_uuid): Path, - Query(pagination): Query, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - let me = Me::get(&mut conn, uuid).await?; - - let members = Member::fetch_page( - &mut conn, - &app_state.cache_pool, - &me, - guild_uuid, - pagination, - ) - .await?; - - Ok((StatusCode::OK, Json(members))) -} diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs deleted file mode 100644 index 53f469b..0000000 --- a/src/api/v1/guilds/uuid/mod.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! `/api/v1/guilds/{uuid}` Specific server endpoints - -use std::sync::Arc; - -use axum::{ - Extension, Json, Router, - extract::{Multipart, Path, State}, - http::StatusCode, - response::IntoResponse, - routing::{delete, get, patch, post}, -}; -use bytes::Bytes; -use uuid::Uuid; - -mod bans; -mod channels; -mod invites; -mod members; -mod roles; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Guild, Member, Permissions}, - utils::global_checks, -}; - -pub fn router() -> Router> { - Router::new() - // Servers - .route("/", get(get_guild)) - .route("/", patch(edit)) - // Channels - .route("/channels", get(channels::get)) - .route("/channels", post(channels::create)) - // Roles - .route("/roles", get(roles::get)) - .route("/roles", post(roles::create)) - .route("/roles/{role_uuid}", get(roles::uuid::get)) - // Invites - .route("/invites", get(invites::get)) - .route("/invites", post(invites::create)) - // Members - .route("/members", get(members::get)) - // Bans - .route("/bans", get(bans::get)) - .route("/bans/{uuid}", delete(bans::unban)) -} - -/// `GET /api/v1/guilds/{uuid}` DESCRIPTION -/// -/// requires auth: yes -/// -/// ### Response Example -/// ``` -/// json!({ -/// "uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", -/// "name": "My first server!", -/// "description": "This is a cool and nullable description!", -/// "icon": "https://nullable-url/path/to/icon.png", -/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", -/// "roles": [ -/// { -/// "uuid": "be0e4da4-cf73-4f45-98f8-bb1c73d1ab8b", -/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", -/// "name": "Cool people", -/// "color": 15650773, -/// "is_above": c7432f1c-f4ad-4ad3-8216-51388b6abb5b, -/// "permissions": 0 -/// } -/// { -/// "uuid": "c7432f1c-f4ad-4ad3-8216-51388b6abb5b", -/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", -/// "name": "Equally cool people", -/// "color": 16777215, -/// "is_above": null, -/// "permissions": 0 -/// } -/// ], -/// "member_count": 20 -/// }); -/// ``` -pub async fn get_guild( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; - - Ok((StatusCode::OK, Json(guild))) -} - -/// `PATCH /api/v1/guilds/{uuid}` change guild settings -/// -/// requires auth: yes -pub async fn edit( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - mut multipart: Multipart, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageGuild) - .await?; - - let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; - - let mut icon: Option = None; - - while let Some(field) = multipart.next_field().await.unwrap() { - let name = field - .name() - .ok_or(Error::BadRequest("Field has no name".to_string()))?; - - if name == "icon" { - icon = Some(field.bytes().await?); - } - } - - if let Some(icon) = icon { - guild.set_icon(&mut conn, &app_state, icon).await?; - } - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs deleted file mode 100644 index d3660ce..0000000 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use serde::Deserialize; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Member, Permissions, Role}, - utils::{CacheFns, global_checks, order_by_is_above}, -}; - -pub mod uuid; - -#[derive(Deserialize)] -pub struct RoleInfo { - name: String, -} - -pub async fn get( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - if let Ok(cache_hit) = app_state - .cache_pool - .get_cache_key::>(format!("{guild_uuid}_roles")) - .await - { - return Ok((StatusCode::OK, Json(cache_hit)).into_response()); - } - - let roles = Role::fetch_all(&mut conn, guild_uuid).await?; - - let roles_ordered = order_by_is_above(roles).await?; - - app_state - .cache_pool - .set_cache_key(format!("{guild_uuid}_roles"), roles_ordered.clone(), 1800) - .await?; - - Ok((StatusCode::OK, Json(roles_ordered)).into_response()) -} - -pub async fn create( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - Json(role_info): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageRole) - .await?; - - let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; - - Ok((StatusCode::OK, Json(role)).into_response()) -} diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs deleted file mode 100644 index e7890d0..0000000 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Member, Role}, - utils::{CacheFns, global_checks}, -}; - -pub async fn get( - State(app_state): State>, - Path((guild_uuid, role_uuid)): Path<(Uuid, Uuid)>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - if let Ok(cache_hit) = app_state - .cache_pool - .get_cache_key::(format!("{role_uuid}")) - .await - { - return Ok((StatusCode::OK, Json(cache_hit)).into_response()); - } - - let role = Role::fetch_one(&mut conn, role_uuid).await?; - - app_state - .cache_pool - .set_cache_key(format!("{role_uuid}"), role.clone(), 60) - .await?; - - Ok((StatusCode::OK, Json(role)).into_response()) -} diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index 99f177f..601d9db 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -1,48 +1,57 @@ -use std::sync::Arc; - -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, post, web}; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, - objects::{Guild, Invite, Member}, - utils::global_checks, + structs::{Guild, Invite, Member}, + utils::get_auth_header, }; +#[get("{id}")] pub async fn get( - State(app_state): State>, - Path(invite_id): Path, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(String,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + check_access_token(auth_header, &mut conn).await?; + + let invite_id = path.into_inner().0; let invite = Invite::fetch_one(&mut conn, invite_id).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; - Ok((StatusCode::OK, Json(guild))) + Ok(HttpResponse::Ok().json(guild)) } +#[post("{id}")] pub async fn join( - State(app_state): State>, - Path(invite_id): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(String,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let invite_id = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; let invite = Invite::fetch_one(&mut conn, invite_id).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; - Member::new(&mut conn, &app_state.cache_pool, uuid, guild.uuid).await?; + Member::new(&mut conn, uuid, guild.uuid).await?; - Ok((StatusCode::OK, Json(guild))) + Ok(HttpResponse::Ok().json(guild)) } diff --git a/src/api/v1/invites/mod.rs b/src/api/v1/invites/mod.rs index 50fb707..3714a83 100644 --- a/src/api/v1/invites/mod.rs +++ b/src/api/v1/invites/mod.rs @@ -1,16 +1,7 @@ -use std::sync::Arc; - -use axum::{ - Router, - routing::{get, post}, -}; - -use crate::AppState; +use actix_web::{Scope, web}; mod id; -pub fn router() -> Router> { - Router::new() - .route("/{id}", get(id::get)) - .route("/{id}", post(id::join)) +pub fn web() -> Scope { + web::scope("/invites").service(id::get).service(id::join) } diff --git a/src/api/v1/me/friends/mod.rs b/src/api/v1/me/friends/mod.rs deleted file mode 100644 index 904a1f5..0000000 --- a/src/api/v1/me/friends/mod.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; -use serde::Deserialize; - -pub mod uuid; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::Me, - utils::{global_checks, user_uuid_from_username}, -}; - -/// Returns a list of users that are your friends -pub async fn get( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let me = Me::get(&mut conn, uuid).await?; - - let friends = me.get_friends(&mut conn, &app_state.cache_pool).await?; - - Ok((StatusCode::OK, Json(friends))) -} - -#[derive(Deserialize)] -pub struct UserReq { - username: String, -} - -/// `POST /api/v1/me/friends` Send friend request -/// -/// requires auth? yes -/// -/// ### Request Example: -/// ``` -/// json!({ -/// "uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", -/// }); -/// ``` -/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -/// -/// ### Responses -/// 200 Success -/// -/// 404 Not Found -/// -/// 400 Bad Request (usually means users are already friends) -/// -pub async fn post( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, - Json(user_request): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let me = Me::get(&mut conn, uuid).await?; - - let target_uuid = user_uuid_from_username(&mut conn, &user_request.username).await?; - me.add_friend(&mut conn, target_uuid).await?; - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/me/friends/pending.rs b/src/api/v1/me/friends/pending.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/api/v1/me/friends/uuid.rs b/src/api/v1/me/friends/uuid.rs deleted file mode 100644 index 35f0742..0000000 --- a/src/api/v1/me/friends/uuid.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::sync::Arc; - -use axum::{ - Extension, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use uuid::Uuid; - -use crate::{ - AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks, -}; - -pub async fn delete( - State(app_state): State>, - Path(friend_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let me = Me::get(&mut conn, uuid).await?; - - me.remove_friend(&mut conn, friend_uuid).await?; - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/me/guilds.rs b/src/api/v1/me/guilds.rs deleted file mode 100644 index 42d5c21..0000000 --- a/src/api/v1/me/guilds.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! `/api/v1/me/guilds` Contains endpoint related to guild memberships - -use std::sync::Arc; - -use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; -use uuid::Uuid; - -use crate::{ - AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks, -}; - -/// `GET /api/v1/me/guilds` Returns all guild memberships in a list -/// -/// requires auth: yes -/// -/// ### Example Response -/// ``` -/// json!([ -/// { -/// "uuid": "383d2afa-082f-4dd3-9050-ca6ed91487b6", -/// "name": "My new server!", -/// "description": null, -/// "icon": null, -/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", -/// "roles": [], -/// "member_count": 1 -/// }, -/// { -/// "uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", -/// "name": "My first server!", -/// "description": "This is a cool and nullable description!", -/// "icon": "https://nullable-url/path/to/icon.png", -/// "owner_uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", -/// "roles": [ -/// { -/// "uuid": "be0e4da4-cf73-4f45-98f8-bb1c73d1ab8b", -/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", -/// "name": "Cool people", -/// "color": 15650773, -/// "is_above": c7432f1c-f4ad-4ad3-8216-51388b6abb5b, -/// "permissions": 0 -/// } -/// { -/// "uuid": "c7432f1c-f4ad-4ad3-8216-51388b6abb5b", -/// "guild_uuid": "5ba61ec7-5f97-43e1-89a5-d4693c155612", -/// "name": "Equally cool people", -/// "color": 16777215, -/// "is_above": null, -/// "permissions": 0 -/// } -/// ], -/// "member_count": 20 -/// } -/// ]); -/// ``` -/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -pub async fn get( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let me = Me::get(&mut conn, uuid).await?; - - let memberships = me.fetch_memberships(&mut conn).await?; - - Ok((StatusCode::OK, Json(memberships))) -} diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs deleted file mode 100644 index 9d75d26..0000000 --- a/src/api/v1/me/mod.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::sync::Arc; - -use axum::{ - Extension, Json, Router, - extract::{DefaultBodyLimit, Multipart, State}, - http::StatusCode, - response::IntoResponse, - routing::{delete, get, patch, post}, -}; -use bytes::Bytes; -use serde::Deserialize; -use uuid::Uuid; - -use crate::{ - AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks, -}; - -mod friends; -mod guilds; - -pub fn router() -> Router> { - Router::new() - .route("/", get(get_me)) - .route( - "/", - patch(update).layer(DefaultBodyLimit::max( - 100 * 1024 * 1024, /* limit is in bytes */ - )), - ) - .route("/guilds", get(guilds::get)) - .route("/friends", get(friends::get)) - .route("/friends", post(friends::post)) - .route("/friends/{uuid}", delete(friends::uuid::delete)) -} - -pub async fn get_me( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; - - Ok((StatusCode::OK, Json(me))) -} - -#[derive(Default, Debug, Deserialize, Clone)] -struct NewInfo { - username: Option, - display_name: Option, - email: Option, - pronouns: Option, - about: Option, - online_status: Option, -} - -pub async fn update( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, - mut multipart: Multipart, -) -> Result { - let mut json_raw: Option = None; - let mut avatar: Option = None; - - while let Some(field) = multipart.next_field().await.unwrap() { - let name = field - .name() - .ok_or(Error::BadRequest("Field has no name".to_string()))?; - - if name == "avatar" { - avatar = Some(field.bytes().await?); - } else if name == "json" { - json_raw = Some(serde_json::from_str(&field.text().await?)?) - } - } - - let json = json_raw.unwrap_or_default(); - - let mut conn = app_state.pool.get().await?; - - if avatar.is_some() || json.username.is_some() || json.display_name.is_some() { - global_checks(&mut conn, &app_state.config, uuid).await?; - } - - let mut me = Me::get(&mut conn, uuid).await?; - - if let Some(avatar) = avatar { - me.set_avatar(&mut conn, &app_state, avatar).await?; - } - - if let Some(username) = &json.username { - me.set_username(&mut conn, &app_state.cache_pool, username.clone()) - .await?; - } - - if let Some(display_name) = &json.display_name { - me.set_display_name(&mut conn, &app_state.cache_pool, display_name.clone()) - .await?; - } - - if let Some(email) = &json.email { - me.set_email(&mut conn, &app_state.cache_pool, email.clone()) - .await?; - } - - if let Some(pronouns) = &json.pronouns { - me.set_pronouns(&mut conn, &app_state.cache_pool, pronouns.clone()) - .await?; - } - - if let Some(about) = &json.about { - me.set_about(&mut conn, &app_state.cache_pool, about.clone()) - .await?; - } - - if let Some(online_status) = &json.online_status { - me.set_online_status(&mut conn, &app_state.cache_pool, *online_status) - .await?; - } - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/members/mod.rs b/src/api/v1/members/mod.rs deleted file mode 100644 index 59ceac2..0000000 --- a/src/api/v1/members/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::sync::Arc; - -use axum::{ - Router, - routing::{delete, get, post}, -}; - -use crate::AppState; - -mod uuid; - -pub fn router() -> Router> { - Router::new() - .route("/{uuid}", get(uuid::get)) - .route("/{uuid}", delete(uuid::delete)) - .route("/{uuid}/ban", post(uuid::ban::post)) -} diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs deleted file mode 100644 index e828e69..0000000 --- a/src/api/v1/members/uuid/ban.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::sync::Arc; - -use axum::{ - Extension, - extract::{Json, Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use serde::Deserialize; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Member, Permissions}, - utils::global_checks, -}; - -use uuid::Uuid; - -#[derive(Deserialize)] -pub struct RequstBody { - reason: String, -} - -pub async fn post( - State(app_state): State>, - Path(member_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - Json(payload): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let member = - Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, None, member_uuid).await?; - - let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; - - caller - .check_permission(&mut conn, &app_state.cache_pool, Permissions::BanMember) - .await?; - - member.ban(&mut conn, &payload.reason).await?; - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs deleted file mode 100644 index 5bfd129..0000000 --- a/src/api/v1/members/uuid/mod.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! `/api/v1/members/{uuid}` Member specific endpoints - -pub mod ban; - -use std::sync::Arc; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Me, Member, Permissions}, - utils::global_checks, -}; -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; - -use uuid::Uuid; - -pub async fn get( - State(app_state): State>, - Path(member_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let me = Me::get(&mut conn, uuid).await?; - - let member = - Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) - .await?; - Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; - - Ok((StatusCode::OK, Json(member))) -} - -pub async fn delete( - State(app_state): State>, - Path(member_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let me = Me::get(&mut conn, uuid).await?; - - let member = - Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) - .await?; - - let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; - - deleter - .check_permission(&mut conn, &app_state.cache_pool, Permissions::KickMember) - .await?; - - member.delete(&mut conn).await?; - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 70271ef..749774d 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -1,35 +1,16 @@ -//! `/api/v1` Contains version 1 of the api - -use std::sync::Arc; - -use axum::{Router, middleware::from_fn_with_state, routing::get}; - -use crate::{AppState, api::v1::auth::CurrentUser}; +use actix_web::{Scope, web}; mod auth; -mod channels; -mod guilds; mod invites; -mod me; -mod members; +mod servers; mod stats; mod users; -pub fn router(app_state: Arc) -> Router> { - let router_with_auth = Router::new() - .nest("/users", users::router()) - .nest("/guilds", guilds::router()) - .nest("/invites", invites::router()) - .nest("/members", members::router()) - .nest("/me", me::router()) - .layer(from_fn_with_state( - app_state.clone(), - CurrentUser::check_auth_layer, - )); - - Router::new() - .route("/stats", get(stats::res)) - .nest("/auth", auth::router(app_state.clone())) - .nest("/channels", channels::router(app_state)) - .merge(router_with_auth) +pub fn web() -> Scope { + web::scope("/v1") + .service(stats::res) + .service(auth::web()) + .service(users::web()) + .service(servers::web()) + .service(invites::web()) } diff --git a/src/api/v1/servers/mod.rs b/src/api/v1/servers/mod.rs new file mode 100644 index 0000000..76a4c16 --- /dev/null +++ b/src/api/v1/servers/mod.rs @@ -0,0 +1,71 @@ +use actix_web::{HttpRequest, HttpResponse, Scope, get, post, web}; +use serde::Deserialize; + +mod uuid; + +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Guild, StartAmountQuery}, + utils::get_auth_header, +}; + +#[derive(Deserialize)] +struct GuildInfo { + name: String, + description: Option, +} + +pub fn web() -> Scope { + web::scope("/servers") + .service(create) + .service(get) + .service(uuid::web()) +} + +#[post("")] +pub async fn create( + req: HttpRequest, + guild_info: web::Json, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + let guild = Guild::new( + &mut conn, + guild_info.name.clone(), + guild_info.description.clone(), + uuid, + ) + .await?; + + Ok(HttpResponse::Ok().json(guild)) +} + +#[get("")] +pub async fn get( + req: HttpRequest, + request_query: web::Query, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let start = request_query.start.unwrap_or(0); + + let amount = request_query.amount.unwrap_or(10); + + check_access_token(auth_header, &mut data.pool.get().await.unwrap()).await?; + + let guilds = Guild::fetch_amount(&data.pool, start, amount).await?; + + Ok(HttpResponse::Ok().json(guilds)) +} diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs new file mode 100644 index 0000000..0c515ef --- /dev/null +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -0,0 +1,82 @@ +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Channel, Member}, + utils::get_auth_header, +}; +use ::uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use serde::Deserialize; + +pub mod uuid; + +#[derive(Deserialize)] +struct ChannelInfo { + name: String, + description: Option, +} + +#[get("{uuid}/channels")] +pub async fn get( + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + if let Ok(cache_hit) = data.get_cache_key(format!("{}_channels", guild_uuid)).await { + return Ok(HttpResponse::Ok() + .content_type("application/json") + .body(cache_hit)); + } + + let channels = Channel::fetch_all(&data.pool, guild_uuid).await?; + + data.set_cache_key(format!("{}_channels", guild_uuid), channels.clone(), 1800) + .await?; + + Ok(HttpResponse::Ok().json(channels)) +} + +#[post("{uuid}/channels")] +pub async fn create( + req: HttpRequest, + channel_info: web::Json, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + // FIXME: Logic to check permissions, should probably be done in utils.rs + + let channel = Channel::new( + data.clone(), + guild_uuid, + channel_info.name.clone(), + channel_info.description.clone(), + ) + .await; + + Ok(HttpResponse::Ok().json(channel.unwrap())) +} diff --git a/src/api/v1/servers/uuid/channels/uuid/messages.rs b/src/api/v1/servers/uuid/channels/uuid/messages.rs new file mode 100644 index 0000000..66ec80d --- /dev/null +++ b/src/api/v1/servers/uuid/channels/uuid/messages.rs @@ -0,0 +1,53 @@ +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Channel, Member}, + utils::get_auth_header, +}; +use ::uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, web}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct MessageRequest { + amount: i64, + offset: i64, +} + +#[get("{uuid}/channels/{channel_uuid}/messages")] +pub async fn get( + req: HttpRequest, + path: web::Path<(Uuid, Uuid)>, + message_request: web::Query, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let (guild_uuid, channel_uuid) = path.into_inner(); + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + let channel: Channel; + + if let Ok(cache_hit) = data.get_cache_key(format!("{}", channel_uuid)).await { + channel = serde_json::from_str(&cache_hit)? + } else { + channel = Channel::fetch_one(&mut conn, channel_uuid).await?; + + data.set_cache_key(format!("{}", channel_uuid), channel.clone(), 60) + .await?; + } + + let messages = channel + .fetch_messages(&mut conn, message_request.amount, message_request.offset) + .await?; + + Ok(HttpResponse::Ok().json(messages)) +} diff --git a/src/api/v1/servers/uuid/channels/uuid/mod.rs b/src/api/v1/servers/uuid/channels/uuid/mod.rs new file mode 100644 index 0000000..54f90a7 --- /dev/null +++ b/src/api/v1/servers/uuid/channels/uuid/mod.rs @@ -0,0 +1,77 @@ +pub mod messages; +pub mod socket; + +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Channel, Member}, + utils::get_auth_header, +}; +use actix_web::{HttpRequest, HttpResponse, delete, get, web}; +use uuid::Uuid; + +#[get("{uuid}/channels/{channel_uuid}")] +pub async fn get( + req: HttpRequest, + path: web::Path<(Uuid, Uuid)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let (guild_uuid, channel_uuid) = path.into_inner(); + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + if let Ok(cache_hit) = data.get_cache_key(format!("{}", channel_uuid)).await { + return Ok(HttpResponse::Ok() + .content_type("application/json") + .body(cache_hit)); + } + + let channel = Channel::fetch_one(&mut conn, channel_uuid).await?; + + data.set_cache_key(format!("{}", channel_uuid), channel.clone(), 60) + .await?; + + Ok(HttpResponse::Ok().json(channel)) +} + +#[delete("{uuid}/channels/{channel_uuid}")] +pub async fn delete( + req: HttpRequest, + path: web::Path<(Uuid, Uuid)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let (guild_uuid, channel_uuid) = path.into_inner(); + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + let channel: Channel; + + if let Ok(cache_hit) = data.get_cache_key(format!("{}", channel_uuid)).await { + channel = serde_json::from_str(&cache_hit).unwrap(); + + data.del_cache_key(format!("{}", channel_uuid)).await?; + } else { + channel = Channel::fetch_one(&mut conn, channel_uuid).await?; + } + + channel.delete(&mut conn).await?; + + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/api/v1/servers/uuid/channels/uuid/socket.rs b/src/api/v1/servers/uuid/channels/uuid/socket.rs new file mode 100644 index 0000000..8938842 --- /dev/null +++ b/src/api/v1/servers/uuid/channels/uuid/socket.rs @@ -0,0 +1,112 @@ +use actix_web::{Error, HttpRequest, HttpResponse, get, rt, web}; +use actix_ws::AggregatedMessage; +use futures_util::StreamExt as _; +use uuid::Uuid; + +use crate::{ + Data, + api::v1::auth::check_access_token, + structs::{Channel, Member}, + utils::get_auth_header, +}; + +#[get("{uuid}/channels/{channel_uuid}/socket")] +pub async fn echo( + req: HttpRequest, + path: web::Path<(Uuid, Uuid)>, + stream: web::Payload, + data: web::Data, +) -> Result { + // Get all headers + let headers = req.headers(); + + // Retrieve auth header + let auth_header = get_auth_header(headers)?; + + // Get uuids from path + let (guild_uuid, channel_uuid) = path.into_inner(); + + let mut conn = data.pool.get().await.map_err(crate::error::Error::from)?; + + // Authorize client using auth header + let uuid = check_access_token(auth_header, &mut conn).await?; + + // Get server member from psql + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + let channel: Channel; + + // Return channel cache or result from psql as `channel` variable + if let Ok(cache_hit) = data.get_cache_key(format!("{}", channel_uuid)).await { + channel = serde_json::from_str(&cache_hit).unwrap() + } else { + channel = Channel::fetch_one(&mut conn, channel_uuid).await?; + + data.set_cache_key(format!("{}", channel_uuid), channel.clone(), 60) + .await?; + } + + let (res, mut session_1, stream) = actix_ws::handle(&req, stream)?; + + let mut stream = stream + .aggregate_continuations() + // aggregate continuation frames up to 1MiB + .max_continuation_size(2_usize.pow(20)); + + let mut pubsub = data + .cache_pool + .get_async_pubsub() + .await + .map_err(crate::error::Error::from)?; + + let mut session_2 = session_1.clone(); + + rt::spawn(async move { + pubsub.subscribe(channel_uuid.to_string()).await.unwrap(); + while let Some(msg) = pubsub.on_message().next().await { + let payload: String = msg.get_payload().unwrap(); + session_1.text(payload).await.unwrap(); + } + }); + + // start task but don't wait for it + rt::spawn(async move { + let mut conn = data + .cache_pool + .get_multiplexed_tokio_connection() + .await + .unwrap(); + // receive messages from websocket + while let Some(msg) = stream.next().await { + match msg { + Ok(AggregatedMessage::Text(text)) => { + // echo text message + redis::cmd("PUBLISH") + .arg(&[channel_uuid.to_string(), text.to_string()]) + .exec_async(&mut conn) + .await + .unwrap(); + channel + .new_message(&mut data.pool.get().await.unwrap(), uuid, text.to_string()) + .await + .unwrap(); + } + + Ok(AggregatedMessage::Binary(bin)) => { + // echo binary message + session_2.binary(bin).await.unwrap(); + } + + Ok(AggregatedMessage::Ping(msg)) => { + // respond to PING frame with PONG frame + session_2.pong(&msg).await.unwrap(); + } + + _ => {} + } + } + }); + + // respond immediately with response connected to WS session + Ok(res) +} diff --git a/src/api/v1/servers/uuid/icon.rs b/src/api/v1/servers/uuid/icon.rs new file mode 100644 index 0000000..66f2e8a --- /dev/null +++ b/src/api/v1/servers/uuid/icon.rs @@ -0,0 +1,48 @@ +use actix_web::{HttpRequest, HttpResponse, put, web}; +use futures_util::StreamExt as _; +use uuid::Uuid; + +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Guild, Member}, + utils::get_auth_header, +}; + +#[put("{uuid}/icon")] +pub async fn upload( + req: HttpRequest, + path: web::Path<(Uuid,)>, + mut payload: web::Payload, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; + + let mut bytes = web::BytesMut::new(); + while let Some(item) = payload.next().await { + bytes.extend_from_slice(&item?); + } + + guild + .set_icon( + &data.storage, + &mut conn, + bytes, + ) + .await?; + + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/api/v1/guilds/uuid/invites/id.rs b/src/api/v1/servers/uuid/invites/id.rs similarity index 100% rename from src/api/v1/guilds/uuid/invites/id.rs rename to src/api/v1/servers/uuid/invites/id.rs diff --git a/src/api/v1/servers/uuid/invites/mod.rs b/src/api/v1/servers/uuid/invites/mod.rs new file mode 100644 index 0000000..13ad378 --- /dev/null +++ b/src/api/v1/servers/uuid/invites/mod.rs @@ -0,0 +1,69 @@ +use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use serde::Deserialize; +use uuid::Uuid; + +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Guild, Member}, + utils::get_auth_header, +}; + +#[derive(Deserialize)] +struct InviteRequest { + custom_id: String, +} + +#[get("{uuid}/invites")] +pub async fn get( + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; + + let invites = guild.get_invites(&mut conn).await?; + + Ok(HttpResponse::Ok().json(invites)) +} + +#[post("{uuid}/invites")] +pub async fn create( + req: HttpRequest, + path: web::Path<(Uuid,)>, + invite_request: web::Json>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + let member = Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; + + let custom_id = invite_request.as_ref().map(|ir| ir.custom_id.clone()); + + let invite = guild.create_invite(&mut conn, &member, custom_id).await?; + + Ok(HttpResponse::Ok().json(invite)) +} diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs new file mode 100644 index 0000000..887fd06 --- /dev/null +++ b/src/api/v1/servers/uuid/mod.rs @@ -0,0 +1,60 @@ +use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; +use uuid::Uuid; + +mod channels; +mod icon; +mod invites; +mod roles; + +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Guild, Member}, + utils::get_auth_header, +}; + +pub fn web() -> Scope { + web::scope("") + // Servers + .service(res) + // Channels + .service(channels::get) + .service(channels::create) + .service(channels::uuid::get) + .service(channels::uuid::delete) + .service(channels::uuid::messages::get) + .service(channels::uuid::socket::echo) + // Roles + .service(roles::get) + .service(roles::create) + .service(roles::uuid::get) + // Invites + .service(invites::get) + .service(invites::create) + // Icon + .service(icon::upload) +} + +#[get("/{uuid}")] +pub async fn res( + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; + + Ok(HttpResponse::Ok().json(guild)) +} diff --git a/src/api/v1/servers/uuid/roles/mod.rs b/src/api/v1/servers/uuid/roles/mod.rs new file mode 100644 index 0000000..fe25d39 --- /dev/null +++ b/src/api/v1/servers/uuid/roles/mod.rs @@ -0,0 +1,76 @@ +use ::uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use serde::Deserialize; + +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Member, Role}, + utils::get_auth_header, +}; + +pub mod uuid; + +#[derive(Deserialize)] +struct RoleInfo { + name: String, +} + +#[get("{uuid}/roles")] +pub async fn get( + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + if let Ok(cache_hit) = data.get_cache_key(format!("{}_roles", guild_uuid)).await { + return Ok(HttpResponse::Ok() + .content_type("application/json") + .body(cache_hit)); + } + + let roles = Role::fetch_all(&mut conn, guild_uuid).await?; + + data.set_cache_key(format!("{}_roles", guild_uuid), roles.clone(), 1800) + .await?; + + Ok(HttpResponse::Ok().json(roles)) +} + +#[post("{uuid}/roles")] +pub async fn create( + req: HttpRequest, + role_info: web::Json, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await.unwrap(); + + let uuid = check_access_token(auth_header, &mut conn).await?; + + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + // FIXME: Logic to check permissions, should probably be done in utils.rs + + let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; + + Ok(HttpResponse::Ok().json(role)) +} diff --git a/src/api/v1/servers/uuid/roles/uuid.rs b/src/api/v1/servers/uuid/roles/uuid.rs new file mode 100644 index 0000000..8ca3cc5 --- /dev/null +++ b/src/api/v1/servers/uuid/roles/uuid.rs @@ -0,0 +1,41 @@ +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Member, Role}, + utils::get_auth_header, +}; +use ::uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, web}; + +#[get("{uuid}/roles/{role_uuid}")] +pub async fn get( + req: HttpRequest, + path: web::Path<(Uuid, Uuid)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let (guild_uuid, role_uuid) = path.into_inner(); + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + Member::fetch_one(&mut conn, uuid, guild_uuid).await?; + + if let Ok(cache_hit) = data.get_cache_key(format!("{}", role_uuid)).await { + return Ok(HttpResponse::Ok() + .content_type("application/json") + .body(cache_hit)); + } + + let role = Role::fetch_one(&mut conn, role_uuid).await?; + + data.set_cache_key(format!("{}", role_uuid), role.clone(), 60) + .await?; + + Ok(HttpResponse::Ok().json(role)) +} diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs index 17c5df6..0b9567e 100644 --- a/src/api/v1/stats.rs +++ b/src/api/v1/stats.rs @@ -1,68 +1,43 @@ -//! `/api/v1/stats` Returns stats about the server - -use std::sync::Arc; use std::time::SystemTime; -use axum::Json; -use axum::extract::State; -use axum::http::StatusCode; -use axum::response::IntoResponse; +use actix_web::{HttpResponse, get, web}; use diesel::QueryDsl; use diesel_async::RunQueryDsl; use serde::Serialize; -use crate::AppState; +use crate::Data; use crate::error::Error; use crate::schema::users::dsl::{users, uuid}; const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); -const GIT_SHORT_HASH: &str = env!("GIT_SHORT_HASH"); #[derive(Serialize)] struct Response { accounts: i64, uptime: u64, version: String, - registration_enabled: bool, - email_verification_required: bool, build_number: String, } -/// `GET /api/v1/stats` Returns stats about the server -/// -/// requires auth: no -/// -/// ### Response Example -/// ``` -/// json!({ -/// "accounts": 3, -/// "uptime": 50000, -/// "version": "0.1.0", -/// "registration_enabled": true, -/// "email_verification_required": true, -/// "build_number": "39d01bb" -/// }); -/// ``` -pub async fn res(State(app_state): State>) -> Result { +#[get("/stats")] +pub async fn res(data: web::Data) -> Result { let accounts: i64 = users .select(uuid) .count() - .get_result(&mut app_state.pool.get().await?) + .get_result(&mut data.pool.get().await?) .await?; let response = Response { // TODO: Get number of accounts from db accounts, uptime: SystemTime::now() - .duration_since(app_state.start_time) + .duration_since(data.start_time) .expect("Seriously why dont you have time??") .as_secs(), version: String::from(VERSION.unwrap_or("UNKNOWN")), - registration_enabled: app_state.config.instance.registration, - email_verification_required: app_state.config.instance.require_email_verification, // TODO: Get build number from git hash or remove this from the spec - build_number: String::from(GIT_SHORT_HASH), + build_number: String::from("how do i implement this?"), }; - Ok((StatusCode::OK, Json(response))) + Ok(HttpResponse::Ok().json(response)) } diff --git a/src/api/v1/users/me.rs b/src/api/v1/users/me.rs new file mode 100644 index 0000000..fab34dd --- /dev/null +++ b/src/api/v1/users/me.rs @@ -0,0 +1,87 @@ +use actix_multipart::form::{MultipartForm, json::Json as MpJson, tempfile::TempFile}; +use actix_web::{HttpRequest, HttpResponse, get, patch, web}; +use serde::Deserialize; + +use crate::{ + Data, api::v1::auth::check_access_token, error::Error, structs::Me, utils::get_auth_header, +}; + +#[get("/me")] +pub async fn res(req: HttpRequest, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + let me = Me::get(&mut conn, uuid).await?; + + Ok(HttpResponse::Ok().json(me)) +} + +#[derive(Debug, Deserialize)] +struct NewInfo { + username: Option, + display_name: Option, + password: Option, + email: Option, +} + +#[derive(Debug, MultipartForm)] +struct UploadForm { + #[multipart(limit = "100MB")] + avatar: Option, + json: Option>, +} + +#[patch("/me")] +pub async fn update( + req: HttpRequest, + MultipartForm(form): MultipartForm, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + let mut me = Me::get(&mut conn, uuid).await?; + + if let Some(avatar) = form.avatar { + let bytes = tokio::fs::read(avatar.file).await?; + + let byte_slice: &[u8] = &bytes; + + me.set_avatar( + &data.storage, + &mut conn, + byte_slice.into(), + ) + .await?; + } + + if let Some(new_info) = form.json { + if let Some(username) = &new_info.username { + todo!(); + } + + if let Some(display_name) = &new_info.display_name { + todo!(); + } + + if let Some(password) = &new_info.password { + todo!(); + } + + if let Some(email) = &new_info.email { + todo!(); + } + } + + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index 999e13f..a0b1ea6 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -1,80 +1,47 @@ -//! `/api/v1/users` Contains endpoints related to all users - -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{ - Extension, Json, Router, - extract::{Query, State}, - http::StatusCode, - response::IntoResponse, - routing::get, -}; +use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, - objects::{StartAmountQuery, User}, - utils::global_checks, + structs::{StartAmountQuery, User}, + utils::get_auth_header, }; +mod me; mod uuid; -pub fn router() -> Router> { - Router::new() - .route("/", get(users)) - .route("/{uuid}", get(uuid::get)) +pub fn web() -> Scope { + web::scope("/users") + .service(res) + .service(me::res) + .service(me::update) + .service(uuid::res) } -/// `GET /api/v1/users` Returns all users on this instance -/// -/// requires auth: yes -/// -/// requires admin: yes -/// -/// ### Response Example -/// ``` -/// json!([ -/// { -/// "uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", -/// "username": "user1", -/// "display_name": "Nullable Name", -/// "avatar": "https://nullable-url.com/path/to/image.png" -/// }, -/// { -/// "uuid": "d48a3317-7b4d-443f-a250-ea9ab2bb8661", -/// "username": "user2", -/// "display_name": "John User 2", -/// "avatar": "https://also-supports-jpg.com/path/to/image.jpg" -/// }, -/// { -/// "uuid": "12c4b3f8-a25b-4b9b-8136-b275c855ed4a", -/// "username": "user3", -/// "display_name": null, -/// "avatar": null -/// } -/// ]); -/// ``` -/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -pub async fn users( - State(app_state): State>, - Query(request_query): Query, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { +#[get("")] +pub async fn res( + req: HttpRequest, + request_query: web::Query, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + let start = request_query.start.unwrap_or(0); let amount = request_query.amount.unwrap_or(10); if amount > 100 { - return Ok(StatusCode::BAD_REQUEST.into_response()); + return Ok(HttpResponse::BadRequest().finish()); } - let mut conn = app_state.pool.get().await?; + let mut conn = data.pool.get().await?; - global_checks(&mut conn, &app_state.config, uuid).await?; + check_access_token(auth_header, &mut conn).await?; let users = User::fetch_amount(&mut conn, start, amount).await?; - Ok((StatusCode::OK, Json(users)).into_response()) + Ok(HttpResponse::Ok().json(users)) } diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index e015c3c..6d9f904 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -1,52 +1,36 @@ -//! `/api/v1/users/{uuid}` Specific user endpoints - -use std::sync::Arc; - -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; +use actix_web::{HttpRequest, HttpResponse, get, web}; use uuid::Uuid; use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Me, User}, - utils::global_checks, + Data, api::v1::auth::check_access_token, error::Error, structs::User, utils::get_auth_header, }; -/// `GET /api/v1/users/{uuid}` Returns user with the given UUID -/// -/// requires auth: yes -/// -/// requires relation: yes -/// -/// ### Response Example -/// ``` -/// json!({ -/// "uuid": "155d2291-fb23-46bd-a656-ae7c5d8218e6", -/// "username": "user1", -/// "display_name": "Nullable Name", -/// "avatar": "https://nullable-url.com/path/to/image.png" -/// }); -/// ``` -/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -pub async fn get( - State(app_state): State>, - Path(user_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; +#[get("/{uuid}")] +pub async fn res( + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let uuid = path.into_inner().0; - let me = Me::get(&mut conn, uuid).await?; + let auth_header = get_auth_header(headers)?; - let user = - User::fetch_one_with_friendship(&mut conn, &app_state.cache_pool, &me, user_uuid).await?; + let mut conn = data.pool.get().await?; - Ok((StatusCode::OK, Json(user))) + check_access_token(auth_header, &mut conn).await?; + + if let Ok(cache_hit) = data.get_cache_key(uuid.to_string()).await { + return Ok(HttpResponse::Ok() + .content_type("application/json") + .body(cache_hit)); + } + + let user = User::fetch_one(&mut conn, uuid).await?; + + data.set_cache_key(uuid.to_string(), user.clone(), 1800) + .await?; + + Ok(HttpResponse::Ok().json(user)) } diff --git a/src/api/versions.rs b/src/api/versions.rs index 3c9576b..e5695be 100644 --- a/src/api/versions.rs +++ b/src/api/versions.rs @@ -1,5 +1,4 @@ -//! `/api/v1/versions` Returns info about api versions -use axum::{Json, http::StatusCode, response::IntoResponse}; +use actix_web::{HttpResponse, Responder, get}; use serde::Serialize; #[derive(Serialize)] @@ -11,25 +10,13 @@ struct Response { #[derive(Serialize)] struct UnstableFeatures; -/// `GET /api/versions` Returns info about api versions. -/// -/// requires auth: no -/// -/// ### Response Example -/// ``` -/// json!({ -/// "unstable_features": {}, -/// "versions": [ -/// "1" -/// ] -/// }); -/// ``` -pub async fn versions() -> impl IntoResponse { +#[get("/versions")] +pub async fn res() -> impl Responder { let response = Response { unstable_features: UnstableFeatures, // TODO: Find a way to dynamically update this possibly? versions: vec![String::from("1")], }; - (StatusCode::OK, Json(response)) + HttpResponse::Ok().json(response) } diff --git a/src/config.rs b/src/config.rs index 2d2c8e0..9cefbe1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,20 +1,16 @@ use crate::error::Error; use bunny_api_tokio::edge_storage::Endpoint; -use lettre::transport::smtp::authentication::Credentials; use log::debug; use serde::Deserialize; use tokio::fs::read_to_string; use url::Url; -use uuid::Uuid; #[derive(Debug, Deserialize)] pub struct ConfigBuilder { database: Database, cache_database: CacheDatabase, web: WebBuilder, - instance: Option, - bunny: BunnyBuilder, - mail: Mail, + bunny: Option, } #[derive(Debug, Deserialize, Clone)] @@ -39,19 +35,10 @@ pub struct CacheDatabase { struct WebBuilder { ip: Option, port: Option, - frontend_url: Url, - backend_url: Option, + url: Url, _ssl: Option, } -#[derive(Debug, Deserialize)] -struct InstanceBuilder { - name: Option, - registration: Option, - require_email_verification: Option, - initial_guild: Option, -} - #[derive(Debug, Deserialize)] struct BunnyBuilder { api_key: String, @@ -60,23 +47,9 @@ struct BunnyBuilder { cdn_url: Url, } -#[derive(Debug, Deserialize, Clone)] -pub struct Mail { - pub smtp: Smtp, - pub address: String, - pub tls: String, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct Smtp { - pub server: String, - username: String, - password: String, -} - impl ConfigBuilder { pub async fn load(path: String) -> Result { - debug!("loading config from: {path}"); + debug!("loading config from: {}", path); let raw = read_to_string(path).await?; let config = toml::from_str(&raw)?; @@ -86,58 +59,42 @@ impl ConfigBuilder { pub fn build(self) -> Config { let web = Web { - ip: self.web.ip.unwrap_or(String::from("0.0.0.0")), - port: self.web.port.unwrap_or(8080), - frontend_url: self.web.frontend_url.clone(), - backend_url: self - .web - .backend_url - .or_else(|| self.web.frontend_url.join("/api").ok()) - .unwrap(), - }; + ip: self.web.ip.unwrap_or(String::from("0.0.0.0")), + port: self.web.port.unwrap_or(8080), + url: self.web.url + }; - let endpoint = match &*self.bunny.endpoint { - "Frankfurt" => Endpoint::Frankfurt, - "London" => Endpoint::London, - "New York" => Endpoint::NewYork, - "Los Angeles" => Endpoint::LosAngeles, - "Singapore" => Endpoint::Singapore, - "Stockholm" => Endpoint::Stockholm, - "Sao Paulo" => Endpoint::SaoPaulo, - "Johannesburg" => Endpoint::Johannesburg, - "Sydney" => Endpoint::Sydney, - url => Endpoint::Custom(url.to_string()), - }; + let bunny; - let bunny = Bunny { - api_key: self.bunny.api_key, - endpoint, - storage_zone: self.bunny.storage_zone, - cdn_url: self.bunny.cdn_url, - }; + if let Some(bunny_builder) = self.bunny { + let endpoint = match &*bunny_builder.endpoint { + "Frankfurt" => Endpoint::Frankfurt, + "London" => Endpoint::London, + "New York" => Endpoint::NewYork, + "Los Angeles" => Endpoint::LosAngeles, + "Singapore" => Endpoint::Singapore, + "Stockholm" => Endpoint::Stockholm, + "Sao Paulo" => Endpoint::SaoPaulo, + "Johannesburg" => Endpoint::Johannesburg, + "Sydney" => Endpoint::Sydney, + url => Endpoint::Custom(url.to_string()), + }; - let instance = match self.instance { - Some(instance) => Instance { - name: instance.name.unwrap_or("Gorb".to_string()), - registration: instance.registration.unwrap_or(true), - require_email_verification: instance.require_email_verification.unwrap_or(false), - initial_guild: instance.initial_guild, - }, - None => Instance { - name: "Gorb".to_string(), - registration: true, - require_email_verification: false, - initial_guild: None, - }, - }; + bunny = Some(Bunny { + api_key: bunny_builder.api_key, + endpoint, + storage_zone: bunny_builder.storage_zone, + cdn_url: bunny_builder.cdn_url, + }); + } else { + bunny = None; + } Config { database: self.database, cache_database: self.cache_database, web, - instance, bunny, - mail: self.mail, } } } @@ -147,25 +104,14 @@ pub struct Config { pub database: Database, pub cache_database: CacheDatabase, pub web: Web, - pub instance: Instance, - pub bunny: Bunny, - pub mail: Mail, + pub bunny: Option, } #[derive(Debug, Clone)] pub struct Web { pub ip: String, pub port: u16, - pub frontend_url: Url, - pub backend_url: Url, -} - -#[derive(Debug, Clone)] -pub struct Instance { - pub name: String, - pub registration: bool, - pub require_email_verification: bool, - pub initial_guild: Option, + pub url: Url, } #[derive(Debug, Clone)] @@ -227,9 +173,3 @@ impl CacheDatabase { url } } - -impl Smtp { - pub fn credentials(&self) -> Credentials { - Credentials::new(self.username.clone(), self.password.clone()) - } -} diff --git a/src/error.rs b/src/error.rs index d6f7a12..135982d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,24 +1,17 @@ use std::{io, time::SystemTimeError}; -use axum::{ - Json, - extract::{ - multipart::MultipartError, - rejection::{JsonRejection, QueryRejection}, - }, +use actix_web::{ + HttpResponse, + error::{PayloadError, ResponseError}, http::{ StatusCode, - header::{InvalidHeaderValue, ToStrError}, + header::{ContentType, ToStrError}, }, - response::IntoResponse, }; use bunny_api_tokio::error::Error as BunnyError; use deadpool::managed::{BuildError, PoolError}; use diesel::{ConnectionError, result::Error as DieselError}; use diesel_async::pooled_connection::PoolError as DieselPoolError; -use lettre::{ - address::AddressError, error::Error as EmailError, transport::smtp::Error as SmtpError, -}; use log::{debug, error}; use redis::RedisError; use serde::Serialize; @@ -58,70 +51,35 @@ pub enum Error { #[error(transparent)] UrlParseError(#[from] url::ParseError), #[error(transparent)] - JsonRejection(#[from] JsonRejection), - #[error(transparent)] - QueryRejection(#[from] QueryRejection), - #[error(transparent)] - MultipartError(#[from] MultipartError), - #[error(transparent)] - InvalidHeaderValue(#[from] InvalidHeaderValue), - #[error(transparent)] - EmailError(#[from] EmailError), - #[error(transparent)] - SmtpError(#[from] SmtpError), - #[error(transparent)] - SmtpAddressError(#[from] AddressError), + PayloadError(#[from] PayloadError), #[error("{0}")] PasswordHashError(String), #[error("{0}")] + PathError(String), + #[error("{0}")] BadRequest(String), #[error("{0}")] Unauthorized(String), - #[error("{0}")] - Forbidden(String), - #[error("{0}")] - TooManyRequests(String), - #[error("{0}")] - InternalServerError(String), - // TODO: remove when doing socket.io - #[error(transparent)] - AxumError(#[from] axum::Error), } -impl IntoResponse for Error { - fn into_response(self) -> axum::response::Response { - let error = match self { - Error::SqlError(DieselError::NotFound) => { - (StatusCode::NOT_FOUND, Json(WebError::new(self.to_string()))) - } - Error::BunnyError(BunnyError::NotFound(_)) => { - (StatusCode::NOT_FOUND, Json(WebError::new(self.to_string()))) - } - Error::BadRequest(_) => ( - StatusCode::BAD_REQUEST, - Json(WebError::new(self.to_string())), - ), - Error::Unauthorized(_) => ( - StatusCode::UNAUTHORIZED, - Json(WebError::new(self.to_string())), - ), - Error::Forbidden(_) => (StatusCode::FORBIDDEN, Json(WebError::new(self.to_string()))), - Error::TooManyRequests(_) => ( - StatusCode::TOO_MANY_REQUESTS, - Json(WebError::new(self.to_string())), - ), - _ => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(WebError::new(self.to_string())), - ), - }; +impl ResponseError for Error { + fn error_response(&self) -> HttpResponse { + debug!("{:?}", self); + error!("{}: {}", self.status_code(), self); - let (code, _) = error; + HttpResponse::build(self.status_code()) + .insert_header(ContentType::json()) + .json(WebError::new(self.to_string())) + } - debug!("{self:?}"); - error!("{code}: {self}"); - - error.into_response() + fn status_code(&self) -> StatusCode { + match *self { + Error::SqlError(DieselError::NotFound) => StatusCode::NOT_FOUND, + Error::BunnyError(BunnyError::NotFound(_)) => StatusCode::NOT_FOUND, + Error::BadRequest(_) => StatusCode::BAD_REQUEST, + Error::Unauthorized(_) => StatusCode::UNAUTHORIZED, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } } } diff --git a/src/main.rs b/src/main.rs index 13e661d..74b1066 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,17 @@ +use actix_cors::Cors; +use actix_files::Files; +use actix_web::{App, HttpServer, web}; use argon2::Argon2; -use axum::{ - Router, - http::{Method, header}, -}; use clap::Parser; -use config::{Config, ConfigBuilder}; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; -use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; use error::Error; -use objects::MailClient; -use std::{sync::Arc, time::SystemTime}; -use tower_http::cors::{AllowOrigin, CorsLayer}; +use simple_logger::SimpleLogger; +use structs::Storage; +use std::time::SystemTime; +mod config; +use config::{Config, ConfigBuilder}; +use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); @@ -19,23 +19,22 @@ type Conn = deadpool::managed::Object>; mod api; -mod config; pub mod error; -pub mod objects; pub mod schema; -//mod socket; +pub mod structs; pub mod utils; -mod wordlist; -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Clone)] #[command(version, about, long_about = None)] struct Args { #[arg(short, long, default_value_t = String::from("/etc/gorb/config.toml"))] config: String, + #[arg(short, long, default_value_t = String::from("/var/lib/gorb/"))] + data_dir: String, } #[derive(Clone)] -pub struct AppState { +pub struct Data { pub pool: deadpool::managed::Pool< AsyncDieselConnectionManager, Conn, @@ -44,14 +43,17 @@ pub struct AppState { pub config: Config, pub argon2: Argon2<'static>, pub start_time: SystemTime, - pub bunny_storage: bunny_api_tokio::EdgeStorageClient, - pub mail_client: MailClient, + pub storage: Storage, } #[tokio::main] async fn main() -> Result<(), Error> { - tracing_subscriber::fmt::init(); - + SimpleLogger::new() + .with_level(log::LevelFilter::Info) + .with_colors(true) + .env() + .init() + .unwrap(); let args = Args::parse(); let config = ConfigBuilder::load(args.config).await?.build(); @@ -65,20 +67,7 @@ async fn main() -> Result<(), Error> { let cache_pool = redis::Client::open(config.cache_database.url())?; - let bunny = config.bunny.clone(); - - let bunny_storage = - bunny_api_tokio::EdgeStorageClient::new(bunny.api_key, bunny.endpoint, bunny.storage_zone) - .await?; - - let mail = config.mail.clone(); - - let mail_client = MailClient::new( - mail.smtp.credentials(), - mail.smtp.server, - mail.address, - mail.tls, - )?; + let storage = Storage::new(config.clone(), args.data_dir.clone()).await?; let database_url = config.database.url(); @@ -112,65 +101,53 @@ async fn main() -> Result<(), Error> { ) */ - let app_state = Arc::new(AppState { + let data = Data { pool, cache_pool, config, // TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk) argon2: Argon2::default(), start_time: SystemTime::now(), - bunny_storage, - mail_client, - }); + storage, + }; - let cors = CorsLayer::new() - // Allow any origin (equivalent to allowed_origin_fn returning true) - .allow_origin(AllowOrigin::predicate(|_origin, _request_head| true)) - .allow_methods(vec![ - Method::GET, - Method::POST, - Method::PUT, - Method::DELETE, - Method::HEAD, - Method::OPTIONS, - Method::CONNECT, - Method::PATCH, - Method::TRACE, - ]) - .allow_headers(vec![ - header::ACCEPT, - header::ACCEPT_LANGUAGE, - header::AUTHORIZATION, - header::CONTENT_LANGUAGE, - header::CONTENT_TYPE, - header::ORIGIN, - header::ACCEPT, - header::COOKIE, - "x-requested-with".parse().unwrap(), - ]) - // Allow credentials - .allow_credentials(true); + let data_dir = args.data_dir.clone(); - /*let (socket_io, io) = SocketIo::builder() - .with_state(app_state.clone()) - .build_layer(); + HttpServer::new(move || { + // Set CORS headers + let cors = Cors::default() + /* + Set Allowed-Control-Allow-Origin header to whatever + the request's Origin header is. Must be done like this + rather than setting it to "*" due to CORS not allowing + sending of credentials (cookies) with wildcard origin. + */ + .allowed_origin_fn(|_origin, _req_head| true) + /* + Allows any request method in CORS preflight requests. + This will be restricted to only ones actually in use later. + */ + .allow_any_method() + /* + Allows any header(s) in request in CORS preflight requests. + This wll be restricted to only ones actually in use later. + */ + .allow_any_header() + /* + Allows browser to include cookies in requests. + This is needed for receiving the secure HttpOnly refresh_token cookie. + */ + .supports_credentials(); - io.ns("/", socket::on_connect); - */ - // build our application with a route - let app = Router::new() - // `GET /` goes to `root` - .merge(api::router( - web.backend_url.path().trim_end_matches("/"), - app_state.clone(), - )) - .with_state(app_state) - //.layer(socket_io) - .layer(cors); - - // run our app with hyper, listening globally on port 3000 - let listener = tokio::net::TcpListener::bind(web.ip + ":" + &web.port.to_string()).await?; - axum::serve(listener, app).await?; + App::new() + .app_data(web::Data::new(data.clone())) + .wrap(cors) + .service(Files::new("/api/assets", &data_dir)) + .service(api::web()) + }) + .bind((web.ip, web.port))? + .run() + .await?; Ok(()) } diff --git a/src/objects/bans.rs b/src/objects/bans.rs deleted file mode 100644 index 602afa6..0000000 --- a/src/objects/bans.rs +++ /dev/null @@ -1,57 +0,0 @@ -use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use diesel_async::RunQueryDsl; - -use crate::{Conn, error::Error, objects::load_or_empty, schema::guild_bans}; - -#[derive(Selectable, Queryable, Serialize, Deserialize)] -#[diesel(table_name = guild_bans)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct GuildBan { - pub guild_uuid: Uuid, - pub user_uuid: Uuid, - pub reason: Option, - pub banned_since: chrono::DateTime, -} - -impl GuildBan { - pub async fn fetch_one( - conn: &mut Conn, - guild_uuid: Uuid, - user_uuid: Uuid, - ) -> Result { - use guild_bans::dsl; - let guild_ban = dsl::guild_bans - .filter(dsl::guild_uuid.eq(guild_uuid)) - .filter(dsl::user_uuid.eq(user_uuid)) - .select(GuildBan::as_select()) - .get_result(conn) - .await?; - - Ok(guild_ban) - } - - pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { - use guild_bans::dsl; - let all_guild_bans = load_or_empty( - dsl::guild_bans - .filter(dsl::guild_uuid.eq(guild_uuid)) - .load(conn) - .await, - )?; - - Ok(all_guild_bans) - } - - pub async fn unban(self, conn: &mut Conn) -> Result<(), Error> { - use guild_bans::dsl; - diesel::delete(guild_bans::table) - .filter(dsl::guild_uuid.eq(self.guild_uuid)) - .filter(dsl::user_uuid.eq(self.user_uuid)) - .execute(conn) - .await?; - Ok(()) - } -} diff --git a/src/objects/channel.rs b/src/objects/channel.rs deleted file mode 100644 index 03a2cf6..0000000 --- a/src/objects/channel.rs +++ /dev/null @@ -1,447 +0,0 @@ -use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, - insert_into, update, -}; -use diesel_async::RunQueryDsl; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{ - Conn, - error::Error, - schema::{channel_permissions, channels, messages}, - utils::{CHANNEL_REGEX, CacheFns, order_by_is_above}, -}; - -use super::{HasIsAbove, HasUuid, Message, load_or_empty, message::MessageBuilder}; - -#[derive(Queryable, Selectable, Insertable, Clone, Debug)] -#[diesel(table_name = channels)] -#[diesel(check_for_backend(diesel::pg::Pg))] -struct ChannelBuilder { - uuid: Uuid, - guild_uuid: Uuid, - name: String, - description: Option, - is_above: Option, -} - -impl ChannelBuilder { - async fn build(self, conn: &mut Conn) -> Result { - use self::channel_permissions::dsl::*; - let channel_permission: Vec = load_or_empty( - channel_permissions - .filter(channel_uuid.eq(self.uuid)) - .select(ChannelPermission::as_select()) - .load(conn) - .await, - )?; - - Ok(Channel { - uuid: self.uuid, - guild_uuid: self.guild_uuid, - name: self.name, - description: self.description, - is_above: self.is_above, - permissions: channel_permission, - }) - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Channel { - pub uuid: Uuid, - pub guild_uuid: Uuid, - name: String, - description: Option, - pub is_above: Option, - pub permissions: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Queryable, Selectable, Debug)] -#[diesel(table_name = channel_permissions)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct ChannelPermission { - pub role_uuid: Uuid, - pub permissions: i64, -} - -impl HasUuid for Channel { - fn uuid(&self) -> &Uuid { - self.uuid.as_ref() - } -} - -impl HasIsAbove for Channel { - fn is_above(&self) -> Option<&Uuid> { - self.is_above.as_ref() - } -} - -impl Channel { - pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { - use channels::dsl; - let channel_builders: Vec = load_or_empty( - dsl::channels - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(ChannelBuilder::as_select()) - .load(conn) - .await, - )?; - - let mut channels = vec![]; - - for builder in channel_builders { - channels.push(builder.build(conn).await?); - } - - Ok(channels) - } - - pub async fn fetch_one( - conn: &mut Conn, - cache_pool: &redis::Client, - channel_uuid: Uuid, - ) -> Result { - if let Ok(cache_hit) = cache_pool.get_cache_key(channel_uuid.to_string()).await { - return Ok(cache_hit); - } - - use channels::dsl; - let channel_builder: ChannelBuilder = dsl::channels - .filter(dsl::uuid.eq(channel_uuid)) - .select(ChannelBuilder::as_select()) - .get_result(conn) - .await?; - - let channel = channel_builder.build(conn).await?; - - cache_pool - .set_cache_key(channel_uuid.to_string(), channel.clone(), 60) - .await?; - - Ok(channel) - } - - pub async fn new( - conn: &mut Conn, - cache_pool: &redis::Client, - guild_uuid: Uuid, - name: String, - description: Option, - ) -> Result { - if !CHANNEL_REGEX.is_match(&name) { - return Err(Error::BadRequest("Channel name is invalid".to_string())); - } - - let channel_uuid = Uuid::now_v7(); - - let channels = Self::fetch_all(conn, guild_uuid).await?; - - let channels_ordered = order_by_is_above(channels).await?; - - let last_channel = channels_ordered.last(); - - let new_channel = ChannelBuilder { - uuid: channel_uuid, - guild_uuid, - name: name.clone(), - description: description.clone(), - is_above: None, - }; - - insert_into(channels::table) - .values(new_channel.clone()) - .execute(conn) - .await?; - - if let Some(old_last_channel) = last_channel { - use channels::dsl; - update(channels::table) - .filter(dsl::uuid.eq(old_last_channel.uuid)) - .set(dsl::is_above.eq(new_channel.uuid)) - .execute(conn) - .await?; - } - - // returns different object because there's no reason to build the channelbuilder (wastes 1 database request) - let channel = Self { - uuid: channel_uuid, - guild_uuid, - name, - description, - is_above: None, - permissions: vec![], - }; - - cache_pool - .set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) - .await?; - - if cache_pool - .get_cache_key::>(format!("{guild_uuid}_channels")) - .await - .is_ok() - { - cache_pool - .del_cache_key(format!("{guild_uuid}_channels")) - .await?; - } - - Ok(channel) - } - - pub async fn delete(self, conn: &mut Conn, cache_pool: &redis::Client) -> Result<(), Error> { - use channels::dsl; - match update(channels::table) - .filter(dsl::is_above.eq(self.uuid)) - .set(dsl::is_above.eq(None::)) - .execute(conn) - .await - { - Ok(r) => Ok(r), - Err(diesel::result::Error::NotFound) => Ok(0), - Err(e) => Err(e), - }?; - - delete(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .execute(conn) - .await?; - - match update(channels::table) - .filter(dsl::is_above.eq(self.uuid)) - .set(dsl::is_above.eq(self.is_above)) - .execute(conn) - .await - { - Ok(r) => Ok(r), - Err(diesel::result::Error::NotFound) => Ok(0), - Err(e) => Err(e), - }?; - - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await?; - } - - if cache_pool - .get_cache_key::>(format!("{}_channels", self.guild_uuid)) - .await - .is_ok() - { - cache_pool - .del_cache_key(format!("{}_channels", self.guild_uuid)) - .await?; - } - - Ok(()) - } - - pub async fn fetch_messages( - &self, - conn: &mut Conn, - cache_pool: &redis::Client, - amount: i64, - offset: i64, - ) -> Result, Error> { - use messages::dsl; - let message_builders: Vec = load_or_empty( - dsl::messages - .filter(dsl::channel_uuid.eq(self.uuid)) - .select(MessageBuilder::as_select()) - .order(dsl::uuid.desc()) - .limit(amount) - .offset(offset) - .load(conn) - .await, - )?; - - let mut messages = vec![]; - - for builder in message_builders { - messages.push(builder.build(conn, cache_pool).await?); - } - - Ok(messages) - } - - pub async fn new_message( - &self, - conn: &mut Conn, - cache_pool: &redis::Client, - user_uuid: Uuid, - message: String, - reply_to: Option, - ) -> Result { - let message_uuid = Uuid::now_v7(); - - let message = MessageBuilder { - uuid: message_uuid, - channel_uuid: self.uuid, - user_uuid, - message, - reply_to, - }; - - insert_into(messages::table) - .values(message.clone()) - .execute(conn) - .await?; - - message.build(conn, cache_pool).await - } - - pub async fn set_name( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_name: String, - ) -> Result<(), Error> { - if !CHANNEL_REGEX.is_match(&new_name) { - return Err(Error::BadRequest("Channel name is invalid".to_string())); - } - - use channels::dsl; - update(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::name.eq(&new_name)) - .execute(conn) - .await?; - - self.name = new_name; - - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await?; - } - - if cache_pool - .get_cache_key::>(format!("{}_channels", self.guild_uuid)) - .await - .is_ok() - { - cache_pool - .del_cache_key(format!("{}_channels", self.guild_uuid)) - .await?; - } - - Ok(()) - } - - pub async fn set_description( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_description: String, - ) -> Result<(), Error> { - use channels::dsl; - update(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::description.eq(&new_description)) - .execute(conn) - .await?; - - self.description = Some(new_description); - - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await?; - } - - if cache_pool - .get_cache_key::>(format!("{}_channels", self.guild_uuid)) - .await - .is_ok() - { - cache_pool - .del_cache_key(format!("{}_channels", self.guild_uuid)) - .await?; - } - - Ok(()) - } - - pub async fn move_channel( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_is_above: Uuid, - ) -> Result<(), Error> { - use channels::dsl; - let old_above_uuid: Option = match dsl::channels - .filter(dsl::is_above.eq(self.uuid)) - .select(dsl::uuid) - .get_result(conn) - .await - { - Ok(r) => Ok(Some(r)), - Err(diesel::result::Error::NotFound) => Ok(None), - Err(e) => Err(e), - }?; - - if let Some(uuid) = old_above_uuid { - update(channels::table) - .filter(dsl::uuid.eq(uuid)) - .set(dsl::is_above.eq(None::)) - .execute(conn) - .await?; - } - - match update(channels::table) - .filter(dsl::is_above.eq(new_is_above)) - .set(dsl::is_above.eq(self.uuid)) - .execute(conn) - .await - { - Ok(r) => Ok(r), - Err(diesel::result::Error::NotFound) => Ok(0), - Err(e) => Err(e), - }?; - - update(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::is_above.eq(new_is_above)) - .execute(conn) - .await?; - - if let Some(uuid) = old_above_uuid { - update(channels::table) - .filter(dsl::uuid.eq(uuid)) - .set(dsl::is_above.eq(self.is_above)) - .execute(conn) - .await?; - } - - self.is_above = Some(new_is_above); - - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await?; - } - - if cache_pool - .get_cache_key::>(format!("{}_channels", self.guild_uuid)) - .await - .is_ok() - { - cache_pool - .del_cache_key(format!("{}_channels", self.guild_uuid)) - .await?; - } - - Ok(()) - } -} diff --git a/src/objects/email_token.rs b/src/objects/email_token.rs deleted file mode 100644 index c826620..0000000 --- a/src/objects/email_token.rs +++ /dev/null @@ -1,72 +0,0 @@ -use chrono::Utc; -use lettre::message::MultiPart; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{ - AppState, - error::Error, - utils::{CacheFns, generate_token}, -}; - -use super::Me; - -#[derive(Serialize, Deserialize)] -pub struct EmailToken { - user_uuid: Uuid, - pub token: String, - pub created_at: chrono::DateTime, -} - -impl EmailToken { - pub async fn get(cache_pool: &redis::Client, user_uuid: Uuid) -> Result { - let email_token = cache_pool - .get_cache_key(format!("{user_uuid}_email_verify")) - .await?; - - Ok(email_token) - } - - #[allow(clippy::new_ret_no_self)] - pub async fn new(app_state: &AppState, me: Me) -> Result<(), Error> { - let token = generate_token::<32>()?; - - let email_token = EmailToken { - user_uuid: me.uuid, - token: token.clone(), - // TODO: Check if this can be replaced with something built into valkey - created_at: Utc::now(), - }; - - app_state - .cache_pool - .set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400) - .await?; - - let mut verify_endpoint = app_state.config.web.frontend_url.join("verify-email")?; - - verify_endpoint.set_query(Some(&format!("token={token}"))); - - let email = app_state - .mail_client - .message_builder() - .to(me.email.parse()?) - .subject(format!("{} E-mail Verification", app_state.config.instance.name)) - .multipart(MultiPart::alternative_plain_html( - format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", app_state.config.instance.name, me.username, verify_endpoint), - format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, app_state.config.instance.name, me.username, verify_endpoint) - ))?; - - app_state.mail_client.send_mail(email).await?; - - Ok(()) - } - - pub async fn delete(&self, cache_pool: &redis::Client) -> Result<(), Error> { - cache_pool - .del_cache_key(format!("{}_email_verify", self.user_uuid)) - .await?; - - Ok(()) - } -} diff --git a/src/objects/friends.rs b/src/objects/friends.rs deleted file mode 100644 index 9d23512..0000000 --- a/src/objects/friends.rs +++ /dev/null @@ -1,24 +0,0 @@ -use chrono::{DateTime, Utc}; -use diesel::{Queryable, Selectable}; -use serde::Serialize; -use uuid::Uuid; - -use crate::schema::{friend_requests, friends}; - -#[derive(Serialize, Queryable, Selectable, Clone)] -#[diesel(table_name = friends)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Friend { - pub uuid1: Uuid, - pub uuid2: Uuid, - pub accepted_at: DateTime, -} - -#[derive(Serialize, Queryable, Selectable, Clone)] -#[diesel(table_name = friend_requests)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct FriendRequest { - pub sender: Uuid, - pub receiver: Uuid, - pub requested_at: DateTime, -} diff --git a/src/objects/guild.rs b/src/objects/guild.rs deleted file mode 100644 index 9640e28..0000000 --- a/src/objects/guild.rs +++ /dev/null @@ -1,215 +0,0 @@ -use axum::body::Bytes; -use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, - update, -}; -use diesel_async::RunQueryDsl; -use serde::Serialize; -use tokio::task; -use url::Url; -use uuid::Uuid; - -use crate::{ - AppState, Conn, - error::Error, - schema::{guild_members, guilds, invites}, - utils::image_check, -}; - -use super::{Invite, Member, Role, load_or_empty, member::MemberBuilder}; - -#[derive(Serialize, Queryable, Selectable, Insertable, Clone)] -#[diesel(table_name = guilds)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct GuildBuilder { - uuid: Uuid, - name: String, - description: Option, - icon: Option, -} - -impl GuildBuilder { - pub async fn build(self, conn: &mut Conn) -> Result { - let member_count = Member::count(conn, self.uuid).await?; - - let roles = Role::fetch_all(conn, self.uuid).await?; - - Ok(Guild { - uuid: self.uuid, - name: self.name, - description: self.description, - icon: self.icon.and_then(|i| i.parse().ok()), - roles, - member_count, - }) - } -} - -#[derive(Serialize)] -pub struct Guild { - pub uuid: Uuid, - name: String, - description: Option, - icon: Option, - pub roles: Vec, - member_count: i64, -} - -impl Guild { - pub async fn fetch_one(conn: &mut Conn, guild_uuid: Uuid) -> Result { - use guilds::dsl; - let guild_builder: GuildBuilder = dsl::guilds - .filter(dsl::uuid.eq(guild_uuid)) - .select(GuildBuilder::as_select()) - .get_result(conn) - .await?; - - guild_builder.build(conn).await - } - - pub async fn fetch_amount( - conn: &mut Conn, - offset: i64, - amount: i64, - ) -> Result, Error> { - // Fetch guild data from database - use guilds::dsl; - let guild_builders: Vec = load_or_empty( - dsl::guilds - .select(GuildBuilder::as_select()) - .order_by(dsl::uuid) - .offset(offset) - .limit(amount) - .load(conn) - .await, - )?; - - let mut guilds = vec![]; - - for builder in guild_builders { - guilds.push(builder.build(conn).await?); - } - - Ok(guilds) - } - - pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result { - let guild_uuid = Uuid::now_v7(); - - let guild_builder = GuildBuilder { - uuid: guild_uuid, - name: name.clone(), - description: None, - icon: None, - }; - - insert_into(guilds::table) - .values(guild_builder) - .execute(conn) - .await?; - - let member_uuid = Uuid::now_v7(); - - let member = MemberBuilder { - uuid: member_uuid, - nickname: None, - user_uuid: owner_uuid, - guild_uuid, - is_owner: true, - }; - - insert_into(guild_members::table) - .values(member) - .execute(conn) - .await?; - - Ok(Guild { - uuid: guild_uuid, - name, - description: None, - icon: None, - roles: vec![], - member_count: 1, - }) - } - - pub async fn get_invites(&self, conn: &mut Conn) -> Result, Error> { - use invites::dsl; - let invites = load_or_empty( - dsl::invites - .filter(dsl::guild_uuid.eq(self.uuid)) - .select(Invite::as_select()) - .load(conn) - .await, - )?; - - Ok(invites) - } - - pub async fn create_invite( - &self, - conn: &mut Conn, - user_uuid: Uuid, - custom_id: Option, - ) -> Result { - let invite_id; - - if let Some(id) = custom_id { - invite_id = id; - if invite_id.len() > 32 { - return Err(Error::BadRequest("MAX LENGTH".to_string())); - } - } else { - let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - - invite_id = random_string::generate(8, charset); - } - - let invite = Invite { - id: invite_id, - user_uuid, - guild_uuid: self.uuid, - }; - - insert_into(invites::table) - .values(invite.clone()) - .execute(conn) - .await?; - - Ok(invite) - } - - // FIXME: Horrible security - pub async fn set_icon( - &mut self, - conn: &mut Conn, - app_state: &AppState, - icon: Bytes, - ) -> Result<(), Error> { - let icon_clone = icon.clone(); - let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??; - - if let Some(icon) = &self.icon { - let relative_url = icon.path().trim_start_matches('/'); - - app_state.bunny_storage.delete(relative_url).await?; - } - - let path = format!("icons/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); - - app_state.bunny_storage.upload(path.clone(), icon).await?; - - let icon_url = app_state.config.bunny.cdn_url.join(&path)?; - - use guilds::dsl; - update(guilds::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::icon.eq(icon_url.as_str())) - .execute(conn) - .await?; - - self.icon = Some(icon_url); - - Ok(()) - } -} diff --git a/src/objects/invite.rs b/src/objects/invite.rs deleted file mode 100644 index 5e0827e..0000000 --- a/src/objects/invite.rs +++ /dev/null @@ -1,30 +0,0 @@ -use diesel::{ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper}; -use diesel_async::RunQueryDsl; -use serde::Serialize; -use uuid::Uuid; - -use crate::{Conn, error::Error, schema::invites}; - -/// Server invite struct -#[derive(Clone, Serialize, Queryable, Selectable, Insertable)] -pub struct Invite { - /// case-sensitive alphanumeric string with a fixed length of 8 characters, can be up to 32 characters for custom invites - pub id: String, - /// User that created the invite - pub user_uuid: Uuid, - /// UUID of the guild that the invite belongs to - pub guild_uuid: Uuid, -} - -impl Invite { - pub async fn fetch_one(conn: &mut Conn, invite_id: String) -> Result { - use invites::dsl; - let invite: Invite = dsl::invites - .filter(dsl::id.eq(invite_id)) - .select(Invite::as_select()) - .get_result(conn) - .await?; - - Ok(invite) - } -} diff --git a/src/objects/me.rs b/src/objects/me.rs deleted file mode 100644 index 167e61e..0000000 --- a/src/objects/me.rs +++ /dev/null @@ -1,486 +0,0 @@ -use axum::body::Bytes; -use diesel::{ - ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, - update, -}; -use diesel_async::RunQueryDsl; -use serde::Serialize; -use tokio::task; -use url::Url; -use uuid::Uuid; - -use crate::{ - AppState, Conn, - error::Error, - objects::{Friend, FriendRequest, User}, - schema::{friend_requests, friends, guild_members, guilds, users}, - utils::{CacheFns, EMAIL_REGEX, USERNAME_REGEX, image_check}, -}; - -use super::{Guild, guild::GuildBuilder, load_or_empty, member::MemberBuilder}; - -#[derive(Serialize, Queryable, Selectable)] -#[diesel(table_name = users)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Me { - pub uuid: Uuid, - pub username: String, - pub display_name: Option, - avatar: Option, - pronouns: Option, - about: Option, - online_status: i16, - pub email: String, - pub email_verified: bool, -} - -impl Me { - pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { - use users::dsl; - let me: Me = dsl::users - .filter(dsl::uuid.eq(user_uuid)) - .select(Me::as_select()) - .get_result(conn) - .await?; - - Ok(me) - } - - pub async fn fetch_memberships(&self, conn: &mut Conn) -> Result, Error> { - use guild_members::dsl; - let memberships: Vec = load_or_empty( - dsl::guild_members - .filter(dsl::user_uuid.eq(self.uuid)) - .select(MemberBuilder::as_select()) - .load(conn) - .await, - )?; - - let mut guilds: Vec = vec![]; - - for membership in memberships { - use guilds::dsl; - guilds.push( - dsl::guilds - .filter(dsl::uuid.eq(membership.guild_uuid)) - .select(GuildBuilder::as_select()) - .get_result(conn) - .await? - .build(conn) - .await?, - ) - } - - Ok(guilds) - } - - pub async fn set_avatar( - &mut self, - conn: &mut Conn, - app_state: &AppState, - avatar: Bytes, - ) -> Result<(), Error> { - let avatar_clone = avatar.clone(); - let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; - - if let Some(avatar) = &self.avatar { - let avatar_url: Url = avatar.parse()?; - - let relative_url = avatar_url.path().trim_start_matches('/'); - - app_state.bunny_storage.delete(relative_url).await?; - } - - let path = format!("avatar/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); - - app_state.bunny_storage.upload(path.clone(), avatar).await?; - - let avatar_url = app_state.config.bunny.cdn_url.join(&path)?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::avatar.eq(avatar_url.as_str())) - .execute(conn) - .await?; - - if app_state - .cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - app_state - .cache_pool - .del_cache_key(self.uuid.to_string()) - .await? - } - - self.avatar = Some(avatar_url.to_string()); - - Ok(()) - } - - pub async fn verify_email(&self, conn: &mut Conn) -> Result<(), Error> { - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::email_verified.eq(true)) - .execute(conn) - .await?; - - Ok(()) - } - - pub async fn set_username( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_username: String, - ) -> Result<(), Error> { - if !USERNAME_REGEX.is_match(&new_username) - || new_username.len() < 3 - || new_username.len() > 32 - { - return Err(Error::BadRequest("Invalid username".to_string())); - } - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::username.eq(new_username.as_str())) - .execute(conn) - .await?; - - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? - } - - self.username = new_username; - - Ok(()) - } - - pub async fn set_display_name( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_display_name: String, - ) -> Result<(), Error> { - let new_display_name_option = if new_display_name.is_empty() { - None - } else { - Some(new_display_name) - }; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::display_name.eq(&new_display_name_option)) - .execute(conn) - .await?; - - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? - } - - self.display_name = new_display_name_option; - - Ok(()) - } - - pub async fn set_email( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_email: String, - ) -> Result<(), Error> { - if !EMAIL_REGEX.is_match(&new_email) { - return Err(Error::BadRequest("Invalid username".to_string())); - } - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(( - dsl::email.eq(new_email.as_str()), - dsl::email_verified.eq(false), - )) - .execute(conn) - .await?; - - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? - } - - self.email = new_email; - - Ok(()) - } - - pub async fn set_pronouns( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_pronouns: String, - ) -> Result<(), Error> { - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set((dsl::pronouns.eq(new_pronouns.as_str()),)) - .execute(conn) - .await?; - - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? - } - - Ok(()) - } - - pub async fn set_about( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_about: String, - ) -> Result<(), Error> { - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set((dsl::about.eq(new_about.as_str()),)) - .execute(conn) - .await?; - - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? - } - - Ok(()) - } - - pub async fn set_online_status( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_status: i16, - ) -> Result<(), Error> { - if !(0..=4).contains(&new_status) { - return Err(Error::BadRequest("Invalid status code".to_string())); - } - self.online_status = new_status; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::online_status.eq(new_status)) - .execute(conn) - .await?; - - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? - } - - Ok(()) - } - - pub async fn friends_with( - &self, - conn: &mut Conn, - user_uuid: Uuid, - ) -> Result, Error> { - use friends::dsl; - - let friends: Vec = if self.uuid < user_uuid { - load_or_empty( - dsl::friends - .filter(dsl::uuid1.eq(self.uuid)) - .filter(dsl::uuid2.eq(user_uuid)) - .load(conn) - .await, - )? - } else { - load_or_empty( - dsl::friends - .filter(dsl::uuid1.eq(user_uuid)) - .filter(dsl::uuid2.eq(self.uuid)) - .load(conn) - .await, - )? - }; - - if friends.is_empty() { - return Ok(None); - } - - Ok(Some(friends[0].clone())) - } - - pub async fn add_friend(&self, conn: &mut Conn, user_uuid: Uuid) -> Result<(), Error> { - if self.friends_with(conn, user_uuid).await?.is_some() { - // TODO: Check if another error should be used - return Err(Error::BadRequest("Already friends with user".to_string())); - } - - use friend_requests::dsl; - - let friend_request: Vec = load_or_empty( - dsl::friend_requests - .filter(dsl::sender.eq(user_uuid)) - .filter(dsl::receiver.eq(self.uuid)) - .load(conn) - .await, - )?; - - #[allow(clippy::get_first)] - if let Some(friend_request) = friend_request.get(0) { - use friends::dsl; - - if self.uuid < user_uuid { - insert_into(friends::table) - .values((dsl::uuid1.eq(self.uuid), dsl::uuid2.eq(user_uuid))) - .execute(conn) - .await?; - } else { - insert_into(friends::table) - .values((dsl::uuid1.eq(user_uuid), dsl::uuid2.eq(self.uuid))) - .execute(conn) - .await?; - } - - use friend_requests::dsl as frdsl; - - delete(friend_requests::table) - .filter(frdsl::sender.eq(friend_request.sender)) - .filter(frdsl::receiver.eq(friend_request.receiver)) - .execute(conn) - .await?; - - Ok(()) - } else { - use friend_requests::dsl; - - insert_into(friend_requests::table) - .values((dsl::sender.eq(self.uuid), dsl::receiver.eq(user_uuid))) - .execute(conn) - .await?; - - Ok(()) - } - } - - pub async fn remove_friend(&self, conn: &mut Conn, user_uuid: Uuid) -> Result<(), Error> { - if self.friends_with(conn, user_uuid).await?.is_none() { - // TODO: Check if another error should be used - return Err(Error::BadRequest("Not friends with user".to_string())); - } - - use friends::dsl; - - if self.uuid < user_uuid { - delete(friends::table) - .filter(dsl::uuid1.eq(self.uuid)) - .filter(dsl::uuid2.eq(user_uuid)) - .execute(conn) - .await?; - } else { - delete(friends::table) - .filter(dsl::uuid1.eq(user_uuid)) - .filter(dsl::uuid2.eq(self.uuid)) - .execute(conn) - .await?; - } - - Ok(()) - } - - pub async fn get_friends( - &self, - conn: &mut Conn, - cache_pool: &redis::Client, - ) -> Result, Error> { - use friends::dsl; - - let friends1 = load_or_empty( - dsl::friends - .filter(dsl::uuid1.eq(self.uuid)) - .select(Friend::as_select()) - .load(conn) - .await, - )?; - - let friends2 = load_or_empty( - dsl::friends - .filter(dsl::uuid2.eq(self.uuid)) - .select(Friend::as_select()) - .load(conn) - .await, - )?; - - let mut friends = vec![]; - - for friend in friends1 { - friends - .push(User::fetch_one_with_friendship(conn, cache_pool, self, friend.uuid2).await?); - } - - for friend in friends2 { - friends - .push(User::fetch_one_with_friendship(conn, cache_pool, self, friend.uuid1).await?); - } - - Ok(friends) - } - - /* TODO - pub async fn get_friend_requests(&self, conn: &mut Conn) -> Result, Error> { - use friend_requests::dsl; - - let friend_request: Vec = load_or_empty( - dsl::friend_requests - .filter(dsl::receiver.eq(self.uuid)) - .load(conn) - .await - )?; - - Ok() - } - - pub async fn delete_friend_request(&self, conn: &mut Conn, user_uuid: Uuid) -> Result, Error> { - use friend_requests::dsl; - - let friend_request: Vec = load_or_empty( - dsl::friend_requests - .filter(dsl::sender.eq(user_uuid)) - .filter(dsl::receiver.eq(self.uuid)) - .load(conn) - .await - )?; - - Ok() - } - */ -} diff --git a/src/objects/member.rs b/src/objects/member.rs deleted file mode 100644 index f7e56da..0000000 --- a/src/objects/member.rs +++ /dev/null @@ -1,378 +0,0 @@ -use diesel::{ - Associations, BoolExpressionMethods, ExpressionMethods, Identifiable, Insertable, JoinOnDsl, - QueryDsl, Queryable, Selectable, SelectableHelper, define_sql_function, delete, insert_into, - sql_types::{Nullable, VarChar}, -}; -use diesel_async::RunQueryDsl; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{ - Conn, - error::Error, - objects::PaginationRequest, - schema::{friends, guild_bans, guild_members, users}, -}; - -use super::{ - Friend, Guild, GuildBan, Me, Pagination, Permissions, Role, User, load_or_empty, - user::UserBuilder, -}; - -define_sql_function! { fn coalesce(x: Nullable, y: Nullable, z: VarChar) -> Text; } - -#[derive(Serialize, Queryable, Identifiable, Selectable, Insertable, Associations)] -#[diesel(table_name = guild_members)] -#[diesel(belongs_to(UserBuilder, foreign_key = user_uuid))] -#[diesel(belongs_to(Guild, foreign_key = guild_uuid))] -#[diesel(primary_key(uuid))] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct MemberBuilder { - pub uuid: Uuid, - pub nickname: Option, - pub user_uuid: Uuid, - pub guild_uuid: Uuid, - pub is_owner: bool, -} - -impl MemberBuilder { - pub async fn build( - &self, - conn: &mut Conn, - cache_pool: &redis::Client, - me: Option<&Me>, - ) -> Result { - let user; - - if let Some(me) = me { - user = User::fetch_one_with_friendship(conn, cache_pool, me, self.user_uuid).await?; - } else { - user = User::fetch_one(conn, cache_pool, self.user_uuid).await?; - } - - let roles = Role::fetch_from_member(conn, cache_pool, self).await?; - - Ok(Member { - uuid: self.uuid, - nickname: self.nickname.clone(), - user_uuid: self.user_uuid, - guild_uuid: self.guild_uuid, - is_owner: self.is_owner, - user, - roles, - }) - } - - async fn build_with_parts( - &self, - conn: &mut Conn, - cache_pool: &redis::Client, - user_builder: UserBuilder, - friend: Option, - ) -> Result { - let mut user = user_builder.build(); - - if let Some(friend) = friend { - user.friends_since = Some(friend.accepted_at); - } - - let roles = Role::fetch_from_member(conn, cache_pool, self).await?; - - Ok(Member { - uuid: self.uuid, - nickname: self.nickname.clone(), - user_uuid: self.user_uuid, - guild_uuid: self.guild_uuid, - is_owner: self.is_owner, - user, - roles, - }) - } - - pub async fn check_permission( - &self, - conn: &mut Conn, - cache_pool: &redis::Client, - permission: Permissions, - ) -> Result<(), Error> { - if !self.is_owner { - let roles = Role::fetch_from_member(conn, cache_pool, self).await?; - let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0); - if !allowed { - return Err(Error::Forbidden("Not allowed".to_string())); - } - } - - Ok(()) - } -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct Member { - pub uuid: Uuid, - pub nickname: Option, - #[serde(skip)] - pub user_uuid: Uuid, - pub guild_uuid: Uuid, - pub is_owner: bool, - user: User, - roles: Vec, -} - -impl Member { - pub async fn count(conn: &mut Conn, guild_uuid: Uuid) -> Result { - use guild_members::dsl; - let count: i64 = dsl::guild_members - .filter(dsl::guild_uuid.eq(guild_uuid)) - .count() - .get_result(conn) - .await?; - - Ok(count) - } - - pub async fn check_membership( - conn: &mut Conn, - user_uuid: Uuid, - guild_uuid: Uuid, - ) -> Result { - use guild_members::dsl; - let member_builder = dsl::guild_members - .filter(dsl::user_uuid.eq(user_uuid)) - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(MemberBuilder::as_select()) - .get_result(conn) - .await?; - - Ok(member_builder) - } - - pub async fn fetch_one( - conn: &mut Conn, - cache_pool: &redis::Client, - me: Option<&Me>, - user_uuid: Uuid, - guild_uuid: Uuid, - ) -> Result { - let member: MemberBuilder; - let user: UserBuilder; - let friend: Option; - use friends::dsl as fdsl; - use guild_members::dsl; - if let Some(me) = me { - (member, user, friend) = dsl::guild_members - .filter(dsl::guild_uuid.eq(guild_uuid)) - .filter(dsl::user_uuid.eq(user_uuid)) - .inner_join(users::table) - .left_join( - fdsl::friends.on(fdsl::uuid1 - .eq(me.uuid) - .and(fdsl::uuid2.eq(users::uuid)) - .or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))), - ) - .select(( - MemberBuilder::as_select(), - UserBuilder::as_select(), - Option::::as_select(), - )) - .get_result(conn) - .await?; - } else { - (member, user) = dsl::guild_members - .filter(dsl::guild_uuid.eq(guild_uuid)) - .filter(dsl::user_uuid.eq(user_uuid)) - .inner_join(users::table) - .select((MemberBuilder::as_select(), UserBuilder::as_select())) - .get_result(conn) - .await?; - - friend = None; - } - - member - .build_with_parts(conn, cache_pool, user, friend) - .await - } - - pub async fn fetch_one_with_uuid( - conn: &mut Conn, - cache_pool: &redis::Client, - me: Option<&Me>, - uuid: Uuid, - ) -> Result { - let member: MemberBuilder; - let user: UserBuilder; - let friend: Option; - use friends::dsl as fdsl; - use guild_members::dsl; - if let Some(me) = me { - (member, user, friend) = dsl::guild_members - .filter(dsl::uuid.eq(uuid)) - .inner_join(users::table) - .left_join( - fdsl::friends.on(fdsl::uuid1 - .eq(me.uuid) - .and(fdsl::uuid2.eq(users::uuid)) - .or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))), - ) - .select(( - MemberBuilder::as_select(), - UserBuilder::as_select(), - Option::::as_select(), - )) - .get_result(conn) - .await?; - } else { - (member, user) = dsl::guild_members - .filter(dsl::uuid.eq(uuid)) - .inner_join(users::table) - .select((MemberBuilder::as_select(), UserBuilder::as_select())) - .get_result(conn) - .await?; - - friend = None; - } - - member - .build_with_parts(conn, cache_pool, user, friend) - .await - } - - pub async fn fetch_page( - conn: &mut Conn, - cache_pool: &redis::Client, - me: &Me, - guild_uuid: Uuid, - pagination: PaginationRequest, - ) -> Result, Error> { - let per_page = pagination.per_page.unwrap_or(50); - let page_multiplier: i64 = ((pagination.page - 1) * per_page).into(); - - if !(10..=100).contains(&per_page) { - return Err(Error::BadRequest( - "Invalid amount per page requested".to_string(), - )); - } - - use friends::dsl as fdsl; - use guild_members::dsl; - let member_builders: Vec<(MemberBuilder, UserBuilder, Option)> = load_or_empty( - dsl::guild_members - .filter(dsl::guild_uuid.eq(guild_uuid)) - .inner_join(users::table) - .left_join( - fdsl::friends.on(fdsl::uuid1 - .eq(me.uuid) - .and(fdsl::uuid2.eq(users::uuid)) - .or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))), - ) - .limit(per_page.into()) - .offset(page_multiplier) - .order_by(coalesce( - dsl::nickname, - users::display_name, - users::username, - )) - .select(( - MemberBuilder::as_select(), - UserBuilder::as_select(), - Option::::as_select(), - )) - .load(conn) - .await, - )?; - - let pages = Member::count(conn, guild_uuid).await? as f32 / per_page as f32; - - let mut members = Pagination:: { - objects: Vec::with_capacity(member_builders.len()), - amount: member_builders.len() as i32, - pages: pages.ceil() as i32, - page: pagination.page, - }; - - for (member, user, friend) in member_builders { - members.objects.push( - member - .build_with_parts(conn, cache_pool, user, friend) - .await?, - ); - } - - Ok(members) - } - - pub async fn new( - conn: &mut Conn, - cache_pool: &redis::Client, - user_uuid: Uuid, - guild_uuid: Uuid, - ) -> Result { - let banned = GuildBan::fetch_one(conn, guild_uuid, user_uuid).await; - - match banned { - Ok(_) => Err(Error::Forbidden("User banned".to_string())), - Err(Error::SqlError(diesel::result::Error::NotFound)) => Ok(()), - Err(e) => Err(e), - }?; - - let member_uuid = Uuid::now_v7(); - - let member = MemberBuilder { - uuid: member_uuid, - guild_uuid, - user_uuid, - nickname: None, - is_owner: false, - }; - - insert_into(guild_members::table) - .values(&member) - .execute(conn) - .await?; - - member.build(conn, cache_pool, None).await - } - - pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { - if self.is_owner { - return Err(Error::Forbidden("Can not kick owner".to_string())); - } - delete(guild_members::table) - .filter(guild_members::uuid.eq(self.uuid)) - .execute(conn) - .await?; - - Ok(()) - } - - pub async fn ban(self, conn: &mut Conn, reason: &String) -> Result<(), Error> { - if self.is_owner { - return Err(Error::Forbidden("Can not ban owner".to_string())); - } - - use guild_bans::dsl; - insert_into(guild_bans::table) - .values(( - dsl::guild_uuid.eq(self.guild_uuid), - dsl::user_uuid.eq(self.user_uuid), - dsl::reason.eq(reason), - )) - .execute(conn) - .await?; - - self.delete(conn).await?; - - Ok(()) - } - - pub fn to_builder(&self) -> MemberBuilder { - MemberBuilder { - uuid: self.uuid, - nickname: self.nickname.clone(), - user_uuid: self.user_uuid, - guild_uuid: self.guild_uuid, - is_owner: self.is_owner, - } - } -} diff --git a/src/objects/message.rs b/src/objects/message.rs deleted file mode 100644 index f30f14d..0000000 --- a/src/objects/message.rs +++ /dev/null @@ -1,61 +0,0 @@ -use diesel::{ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable}; -use diesel_async::RunQueryDsl; -use serde::Serialize; -use uuid::Uuid; - -use crate::{ - Conn, - error::Error, - schema::{channels, guilds, messages}, -}; - -use super::Member; - -#[derive(Clone, Queryable, Selectable, Insertable)] -#[diesel(table_name = messages)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct MessageBuilder { - pub uuid: Uuid, - pub channel_uuid: Uuid, - pub user_uuid: Uuid, - pub message: String, - pub reply_to: Option, -} - -impl MessageBuilder { - pub async fn build( - &self, - conn: &mut Conn, - cache_pool: &redis::Client, - ) -> Result { - use channels::dsl; - - let guild_uuid = dsl::channels - .filter(dsl::uuid.eq(self.channel_uuid)) - .inner_join(guilds::table) - .select(guilds::uuid) - .get_result(conn) - .await?; - - let member = Member::fetch_one(conn, cache_pool, None, self.user_uuid, guild_uuid).await?; - - Ok(Message { - uuid: self.uuid, - channel_uuid: self.channel_uuid, - user_uuid: self.user_uuid, - message: self.message.clone(), - reply_to: self.reply_to, - member, - }) - } -} - -#[derive(Clone, Serialize)] -pub struct Message { - uuid: Uuid, - channel_uuid: Uuid, - user_uuid: Uuid, - message: String, - reply_to: Option, - member: Member, -} diff --git a/src/objects/mod.rs b/src/objects/mod.rs deleted file mode 100644 index 5a013ca..0000000 --- a/src/objects/mod.rs +++ /dev/null @@ -1,169 +0,0 @@ -use lettre::{ - AsyncSmtpTransport, AsyncTransport, Message as Email, Tokio1Executor, - message::{Mailbox, MessageBuilder as EmailBuilder}, - transport::smtp::authentication::Credentials, -}; -use log::debug; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -mod bans; -mod channel; -mod email_token; -mod friends; -mod guild; -mod invite; -mod me; -mod member; -mod message; -mod password_reset_token; -mod role; -mod user; - -pub use bans::GuildBan; -pub use channel::Channel; -pub use email_token::EmailToken; -pub use friends::Friend; -pub use friends::FriendRequest; -pub use guild::Guild; -pub use invite::Invite; -pub use me::Me; -pub use member::Member; -pub use message::Message; -pub use password_reset_token::PasswordResetToken; -pub use role::Permissions; -pub use role::Role; -pub use user::User; - -use crate::error::Error; - -pub trait HasUuid { - fn uuid(&self) -> &Uuid; -} - -pub trait HasIsAbove { - fn is_above(&self) -> Option<&Uuid>; -} -/* -pub trait Cookies { - fn cookies(&self) -> CookieJar; - fn cookie>(&self, cookie: T) -> Option; -} - -impl Cookies for Request { - fn cookies(&self) -> CookieJar { - let cookies = self.headers() - .get(axum::http::header::COOKIE) - .and_then(|value| value.to_str().ok()) - .map(|s| Cookie::split_parse(s.to_string())) - .and_then(|c| c.collect::, cookie::ParseError>>().ok()) - .unwrap_or(vec![]); - - let mut cookie_jar = CookieJar::new(); - - for cookie in cookies { - cookie_jar.add(cookie) - } - - cookie_jar - } - - fn cookie>(&self, cookie: T) -> Option { - self.cookies() - .get(cookie.as_ref()) - .and_then(|c| Some(c.to_owned())) - } -} -*/ - -#[derive(Serialize)] -pub struct Pagination { - objects: Vec, - amount: i32, - pages: i32, - page: i32, -} - -#[derive(Deserialize)] -pub struct PaginationRequest { - pub page: i32, - pub per_page: Option, -} - -fn load_or_empty( - query_result: Result, diesel::result::Error>, -) -> Result, diesel::result::Error> { - match query_result { - Ok(vec) => Ok(vec), - Err(diesel::result::Error::NotFound) => Ok(Vec::new()), - Err(e) => Err(e), - } -} - -#[derive(PartialEq, Eq, Clone)] -pub enum MailTls { - StartTls, - Tls, -} - -impl From for MailTls { - fn from(value: String) -> Self { - match &*value.to_lowercase() { - "starttls" => Self::StartTls, - _ => Self::Tls, - } - } -} - -#[derive(Clone)] -pub struct MailClient { - creds: Credentials, - smtp_server: String, - mbox: Mailbox, - tls: MailTls, -} - -impl MailClient { - pub fn new>( - creds: Credentials, - smtp_server: String, - mbox: String, - tls: T, - ) -> Result { - Ok(Self { - creds, - smtp_server, - mbox: mbox.parse()?, - tls: tls.into(), - }) - } - - pub fn message_builder(&self) -> EmailBuilder { - Email::builder().from(self.mbox.clone()) - } - - pub async fn send_mail(&self, email: Email) -> Result<(), Error> { - let mailer: AsyncSmtpTransport = match self.tls { - MailTls::StartTls => { - AsyncSmtpTransport::::starttls_relay(&self.smtp_server)? - .credentials(self.creds.clone()) - .build() - } - MailTls::Tls => AsyncSmtpTransport::::relay(&self.smtp_server)? - .credentials(self.creds.clone()) - .build(), - }; - - let response = mailer.send(email).await?; - - debug!("mail sending response: {response:?}"); - - Ok(()) - } -} - -#[derive(Deserialize)] -pub struct StartAmountQuery { - pub start: Option, - pub amount: Option, -} diff --git a/src/objects/password_reset_token.rs b/src/objects/password_reset_token.rs deleted file mode 100644 index ca5c62f..0000000 --- a/src/objects/password_reset_token.rs +++ /dev/null @@ -1,167 +0,0 @@ -use argon2::{ - PasswordHasher, - password_hash::{SaltString, rand_core::OsRng}, -}; -use chrono::Utc; -use diesel::{ExpressionMethods, QueryDsl, update}; -use diesel_async::RunQueryDsl; -use lettre::message::MultiPart; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{ - AppState, Conn, - error::Error, - schema::users, - utils::{CacheFns, PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier}, -}; - -#[derive(Serialize, Deserialize)] -pub struct PasswordResetToken { - user_uuid: Uuid, - pub token: String, - pub created_at: chrono::DateTime, -} - -impl PasswordResetToken { - pub async fn get( - cache_pool: &redis::Client, - token: String, - ) -> Result { - let user_uuid: Uuid = cache_pool.get_cache_key(token.to_string()).await?; - let password_reset_token = cache_pool - .get_cache_key(format!("{user_uuid}_password_reset")) - .await?; - - Ok(password_reset_token) - } - - pub async fn get_with_identifier( - conn: &mut Conn, - cache_pool: &redis::Client, - identifier: String, - ) -> Result { - let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; - - let password_reset_token = cache_pool - .get_cache_key(format!("{user_uuid}_password_reset")) - .await?; - - Ok(password_reset_token) - } - - #[allow(clippy::new_ret_no_self)] - pub async fn new( - conn: &mut Conn, - app_state: &AppState, - identifier: String, - ) -> Result<(), Error> { - let token = generate_token::<32>()?; - - let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; - - global_checks(conn, &app_state.config, user_uuid).await?; - - use users::dsl as udsl; - let (username, email_address): (String, String) = udsl::users - .filter(udsl::uuid.eq(user_uuid)) - .select((udsl::username, udsl::email)) - .get_result(conn) - .await?; - - let password_reset_token = PasswordResetToken { - user_uuid, - token: token.clone(), - created_at: Utc::now(), - }; - - app_state - .cache_pool - .set_cache_key( - format!("{user_uuid}_password_reset"), - password_reset_token, - 86400, - ) - .await?; - app_state - .cache_pool - .set_cache_key(token.clone(), user_uuid, 86400) - .await?; - - let mut reset_endpoint = app_state.config.web.frontend_url.join("reset-password")?; - - reset_endpoint.set_query(Some(&format!("token={token}"))); - - let email = app_state - .mail_client - .message_builder() - .to(email_address.parse()?) - .subject(format!("{} Password Reset", app_state.config.instance.name)) - .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", app_state.config.instance.name, username, reset_endpoint), - format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, app_state.config.instance.name, username, reset_endpoint) - ))?; - - app_state.mail_client.send_mail(email).await?; - - Ok(()) - } - - pub async fn set_password( - &self, - conn: &mut Conn, - app_state: &AppState, - password: String, - ) -> Result<(), Error> { - if !PASSWORD_REGEX.is_match(&password) { - return Err(Error::BadRequest( - "Please provide a valid password".to_string(), - )); - } - - let salt = SaltString::generate(&mut OsRng); - - let hashed_password = app_state - .argon2 - .hash_password(password.as_bytes(), &salt) - .map_err(|e| Error::PasswordHashError(e.to_string()))?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.user_uuid)) - .set(dsl::password.eq(hashed_password.to_string())) - .execute(conn) - .await?; - - let (username, email_address): (String, String) = dsl::users - .filter(dsl::uuid.eq(self.user_uuid)) - .select((dsl::username, dsl::email)) - .get_result(conn) - .await?; - - let login_page = app_state.config.web.frontend_url.join("login")?; - - let email = app_state - .mail_client - .message_builder() - .to(email_address.parse()?) - .subject(format!("Your {} Password has been Reset", app_state.config.instance.name)) - .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", app_state.config.instance.name, username, login_page), - format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, app_state.config.instance.name, username, login_page) - ))?; - - app_state.mail_client.send_mail(email).await?; - - self.delete(&app_state.cache_pool).await - } - - pub async fn delete(&self, cache_pool: &redis::Client) -> Result<(), Error> { - cache_pool - .del_cache_key(format!("{}_password_reset", &self.user_uuid)) - .await?; - cache_pool.del_cache_key(self.token.to_string()).await?; - - Ok(()) - } -} diff --git a/src/objects/role.rs b/src/objects/role.rs deleted file mode 100644 index cc71fb2..0000000 --- a/src/objects/role.rs +++ /dev/null @@ -1,188 +0,0 @@ -use diesel::query_dsl::BelongingToDsl; -use diesel::{ - Associations, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, Selectable, - SelectableHelper, insert_into, update, -}; -use diesel_async::RunQueryDsl; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{ - Conn, - error::Error, - schema::{role_members, roles}, - utils::{CacheFns, order_by_is_above}, -}; - -use super::{HasIsAbove, HasUuid, load_or_empty, member::MemberBuilder}; - -#[derive(Deserialize, Serialize, Clone, Identifiable, Queryable, Selectable, Insertable)] -#[diesel(table_name = roles)] -#[diesel(primary_key(uuid))] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Role { - uuid: Uuid, - guild_uuid: Uuid, - name: String, - color: i32, - is_above: Option, - pub permissions: i64, -} - -#[derive(Serialize, Clone, Identifiable, Queryable, Selectable, Insertable, Associations)] -#[diesel(table_name = role_members)] -#[diesel(belongs_to(MemberBuilder, foreign_key = member_uuid))] -#[diesel(belongs_to(Role, foreign_key = role_uuid))] -#[diesel(primary_key(role_uuid, member_uuid))] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct RoleMember { - role_uuid: Uuid, - member_uuid: Uuid, -} - -impl HasUuid for Role { - fn uuid(&self) -> &Uuid { - self.uuid.as_ref() - } -} - -impl HasIsAbove for Role { - fn is_above(&self) -> Option<&Uuid> { - self.is_above.as_ref() - } -} - -impl Role { - pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { - use roles::dsl; - let roles: Vec = load_or_empty( - dsl::roles - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(Role::as_select()) - .load(conn) - .await, - )?; - - Ok(roles) - } - - pub async fn fetch_from_member( - conn: &mut Conn, - cache_pool: &redis::Client, - member: &MemberBuilder, - ) -> Result, Error> { - if let Ok(roles) = cache_pool - .get_cache_key(format!("{}_roles", member.uuid)) - .await - { - return Ok(roles); - } - - let roles: Vec = load_or_empty( - RoleMember::belonging_to(member) - .inner_join(roles::table) - .select(Role::as_select()) - .load(conn) - .await, - )?; - - cache_pool - .set_cache_key(format!("{}_roles", member.uuid), roles.clone(), 300) - .await?; - - Ok(roles) - } - - pub async fn fetch_one(conn: &mut Conn, role_uuid: Uuid) -> Result { - use roles::dsl; - let role: Role = dsl::roles - .filter(dsl::uuid.eq(role_uuid)) - .select(Role::as_select()) - .get_result(conn) - .await?; - - Ok(role) - } - - pub async fn fetch_permissions(&self) -> Vec { - Permissions::fetch_permissions(self.permissions) - } - - pub async fn new(conn: &mut Conn, guild_uuid: Uuid, name: String) -> Result { - let role_uuid = Uuid::now_v7(); - - let roles = Self::fetch_all(conn, guild_uuid).await?; - - let roles_ordered = order_by_is_above(roles).await?; - - let last_role = roles_ordered.last(); - - let new_role = Role { - uuid: role_uuid, - guild_uuid, - name, - color: 16777215, - is_above: None, - permissions: 0, - }; - - insert_into(roles::table) - .values(new_role.clone()) - .execute(conn) - .await?; - - if let Some(old_last_role) = last_role { - use roles::dsl; - update(roles::table) - .filter(dsl::uuid.eq(old_last_role.uuid)) - .set(dsl::is_above.eq(new_role.uuid)) - .execute(conn) - .await?; - } - - Ok(new_role) - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum Permissions { - /// Lets users send messages in the guild or channel - SendMessage = 1, - /// Lets users create, delete and edit channels and categories or a singular channel depending on permission context - ManageChannel = 2, - /// Lets users manage roles in the guild - ManageRole = 4, - /// Lets users create invites in the guild - CreateInvite = 8, - /// Lets users manage invites in the guild - ManageInvite = 16, - /// Lets users change guild settings - ManageGuild = 32, - /// Lets users change member settings (nickname, etc) - ManageMember = 64, - /// Lets users ban members - BanMember = 128, - /// Lets users kick members - KickMember = 256, -} - -impl Permissions { - pub fn fetch_permissions(permissions: i64) -> Vec { - let all_perms = vec![ - Self::SendMessage, - Self::ManageChannel, - Self::ManageRole, - Self::CreateInvite, - Self::ManageInvite, - Self::ManageGuild, - Self::ManageMember, - Self::BanMember, - Self::KickMember, - ]; - - all_perms - .into_iter() - .filter(|p| permissions & (*p as i64) != 0) - .collect() - } -} diff --git a/src/objects/user.rs b/src/objects/user.rs deleted file mode 100644 index c91b809..0000000 --- a/src/objects/user.rs +++ /dev/null @@ -1,111 +0,0 @@ -use chrono::{DateTime, Utc}; -use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; -use diesel_async::RunQueryDsl; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{Conn, error::Error, objects::Me, schema::users, utils::CacheFns}; - -use super::load_or_empty; - -#[derive(Deserialize, Serialize, Clone, Queryable, Selectable)] -#[diesel(table_name = users)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct UserBuilder { - uuid: Uuid, - username: String, - display_name: Option, - avatar: Option, - pronouns: Option, - about: Option, - online_status: i16, -} - -impl UserBuilder { - pub fn build(self) -> User { - User { - uuid: self.uuid, - username: self.username, - display_name: self.display_name, - avatar: self.avatar, - pronouns: self.pronouns, - about: self.about, - online_status: self.online_status, - friends_since: None, - } - } -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct User { - pub uuid: Uuid, - username: String, - display_name: Option, - avatar: Option, - pronouns: Option, - about: Option, - online_status: i16, - pub friends_since: Option>, -} - -impl User { - pub async fn fetch_one( - conn: &mut Conn, - cache_pool: &redis::Client, - user_uuid: Uuid, - ) -> Result { - if let Ok(cache_hit) = cache_pool.get_cache_key(user_uuid.to_string()).await { - return Ok(cache_hit); - } - - use users::dsl; - let user_builder: UserBuilder = dsl::users - .filter(dsl::uuid.eq(user_uuid)) - .select(UserBuilder::as_select()) - .get_result(conn) - .await?; - - let user = user_builder.build(); - - cache_pool - .set_cache_key(user_uuid.to_string(), user.clone(), 1800) - .await?; - - Ok(user) - } - - pub async fn fetch_one_with_friendship( - conn: &mut Conn, - cache_pool: &redis::Client, - me: &Me, - user_uuid: Uuid, - ) -> Result { - let mut user = Self::fetch_one(conn, cache_pool, user_uuid).await?; - - if let Some(friend) = me.friends_with(conn, user_uuid).await? { - user.friends_since = Some(friend.accepted_at); - } - - Ok(user) - } - - pub async fn fetch_amount( - conn: &mut Conn, - offset: i64, - amount: i64, - ) -> Result, Error> { - use users::dsl; - let user_builders: Vec = load_or_empty( - dsl::users - .limit(amount) - .offset(offset) - .select(UserBuilder::as_select()) - .load(conn) - .await, - )?; - - let users: Vec = user_builders.iter().map(|u| u.clone().build()).collect(); - - Ok(users) - } -} diff --git a/src/schema.rs b/src/schema.rs index 88f6155..33935f7 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -27,33 +27,6 @@ diesel::table! { name -> Varchar, #[max_length = 500] description -> Nullable, - is_above -> Nullable, - } -} - -diesel::table! { - friend_requests (sender, receiver) { - sender -> Uuid, - receiver -> Uuid, - requested_at -> Timestamptz, - } -} - -diesel::table! { - friends (uuid1, uuid2) { - uuid1 -> Uuid, - uuid2 -> Uuid, - accepted_at -> Timestamptz, - } -} - -diesel::table! { - guild_bans (user_uuid, guild_uuid) { - guild_uuid -> Uuid, - user_uuid -> Uuid, - #[max_length = 200] - reason -> Nullable, - banned_since -> Timestamptz, } } @@ -64,18 +37,18 @@ diesel::table! { user_uuid -> Uuid, #[max_length = 100] nickname -> Nullable, - is_owner -> Bool, } } diesel::table! { guilds (uuid) { uuid -> Uuid, + owner_uuid -> Uuid, #[max_length = 100] name -> Varchar, #[max_length = 300] description -> Nullable, - #[max_length = 8000] + #[max_length = 100] icon -> Nullable, } } @@ -103,7 +76,6 @@ diesel::table! { user_uuid -> Uuid, #[max_length = 4000] message -> Varchar, - reply_to -> Nullable, } } @@ -113,7 +85,7 @@ diesel::table! { token -> Varchar, uuid -> Uuid, created_at -> Int8, - #[max_length = 64] + #[max_length = 16] device_name -> Varchar, } } @@ -126,14 +98,14 @@ diesel::table! { } diesel::table! { - roles (uuid) { + roles (uuid, guild_uuid) { uuid -> Uuid, guild_uuid -> Uuid, #[max_length = 50] name -> Varchar, color -> Int4, + position -> Int4, permissions -> Int8, - is_above -> Nullable, } } @@ -151,25 +123,18 @@ diesel::table! { email_verified -> Bool, is_deleted -> Bool, deleted_at -> Nullable, - #[max_length = 8000] + #[max_length = 100] avatar -> Nullable, - #[max_length = 32] - pronouns -> Nullable, - #[max_length = 200] - about -> Nullable, - online_status -> Int2, } } diesel::joinable!(access_tokens -> refresh_tokens (refresh_token)); diesel::joinable!(access_tokens -> users (uuid)); diesel::joinable!(channel_permissions -> channels (channel_uuid)); -diesel::joinable!(channel_permissions -> roles (role_uuid)); diesel::joinable!(channels -> guilds (guild_uuid)); -diesel::joinable!(guild_bans -> guilds (guild_uuid)); -diesel::joinable!(guild_bans -> users (user_uuid)); diesel::joinable!(guild_members -> guilds (guild_uuid)); diesel::joinable!(guild_members -> users (user_uuid)); +diesel::joinable!(guilds -> users (owner_uuid)); diesel::joinable!(instance_permissions -> users (uuid)); diesel::joinable!(invites -> guilds (guild_uuid)); diesel::joinable!(invites -> users (user_uuid)); @@ -177,16 +142,12 @@ diesel::joinable!(messages -> channels (channel_uuid)); diesel::joinable!(messages -> users (user_uuid)); diesel::joinable!(refresh_tokens -> users (uuid)); diesel::joinable!(role_members -> guild_members (member_uuid)); -diesel::joinable!(role_members -> roles (role_uuid)); diesel::joinable!(roles -> guilds (guild_uuid)); diesel::allow_tables_to_appear_in_same_query!( access_tokens, channel_permissions, channels, - friend_requests, - friends, - guild_bans, guild_members, guilds, instance_permissions, diff --git a/src/socket.rs b/src/socket.rs deleted file mode 100644 index 3fcae32..0000000 --- a/src/socket.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::sync::Arc; - -use log::info; -use rmpv::Value; -use socketioxide::extract::{AckSender, Data, SocketRef, State}; - -use crate::AppState; - -pub async fn on_connect( - State(_app_state): State>, - socket: SocketRef, - Data(data): Data, -) { - socket.emit("auth", &data).ok(); - - socket.on("message", async |socket: SocketRef, Data::(data)| { - info!("{data}"); - socket.emit("message-back", &data).ok(); - }); - - socket.on( - "message-with-ack", - async |Data::(data), ack: AckSender| { - info!("{data}"); - ack.send(&data).ok(); - }, - ); -} diff --git a/src/structs.rs b/src/structs.rs new file mode 100644 index 0000000..92bc599 --- /dev/null +++ b/src/structs.rs @@ -0,0 +1,781 @@ +use std::path::Path; + +use actix_web::web::BytesMut; +use diesel::{ + ExpressionMethods, QueryDsl, Selectable, SelectableHelper, delete, insert_into, + prelude::{Insertable, Queryable}, + update, +}; +use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; +use serde::{Deserialize, Serialize}; +use tokio::{fs::{create_dir_all, remove_file, File}, io::AsyncWriteExt, task}; +use url::Url; +use uuid::Uuid; + +use crate::{config::Config, error::Error, schema::*, utils::image_check, Conn, Data}; + +fn load_or_empty( + query_result: Result, diesel::result::Error>, +) -> Result, diesel::result::Error> { + match query_result { + Ok(vec) => Ok(vec), + Err(diesel::result::Error::NotFound) => Ok(Vec::new()), + Err(e) => Err(e), + } +} + +#[derive(Clone)] +pub struct Storage { + bunny_cdn: Option, + cdn_url: Url, + data_dir: String, +} + +impl Storage { + pub async fn new(config: Config, data_dir: String) -> Result { + let mut bunny_cdn; + let cdn_url; + + if let Some(bunny) = config.bunny { + bunny_cdn = Some(bunny_api_tokio::Client::new("").await?); + + bunny_cdn + .as_mut() + .unwrap() + .storage + .init(bunny.api_key, bunny.endpoint, bunny.storage_zone) + .await?; + + cdn_url = bunny.cdn_url; + } else { + bunny_cdn = None; + cdn_url = config.web.url.join("api/assets/")?; + } + + Ok(Self { + bunny_cdn, + data_dir, + cdn_url + }) + } + + pub async fn write>(&self, path: T, bytes: BytesMut) -> Result { + if let Some(bunny_cdn) = &self.bunny_cdn { + bunny_cdn.storage.upload(&path, bytes.into()).await?; + Ok(self.cdn_url.join(&path.as_ref())?) + } else { + let file_path = Path::new(&self.data_dir); + + let file_path = file_path.join(path.as_ref()); + + create_dir_all(file_path.parent().ok_or(Error::PathError("Unable to get parent directory".to_string()))?).await?; + + let mut file = File::create(file_path).await?; + + file.write_all(&bytes).await?; + + Ok(self.cdn_url.join(path.as_ref())?) + } + } + + pub async fn delete>(&self, path: T) -> Result<(), Error> { + if let Some(bunny_cdn) = &self.bunny_cdn { + Ok(bunny_cdn.storage.delete(&path).await?) + } else { + let file_path = Path::new(&self.data_dir); + + let file_path = file_path.join(path.as_ref()); + + remove_file(file_path).await?; + Ok(()) + } + } +} + +#[derive(Queryable, Selectable, Insertable, Clone)] +#[diesel(table_name = channels)] +#[diesel(check_for_backend(diesel::pg::Pg))] +struct ChannelBuilder { + uuid: Uuid, + guild_uuid: Uuid, + name: String, + description: Option, +} + +impl ChannelBuilder { + async fn build(self, conn: &mut Conn) -> Result { + use self::channel_permissions::dsl::*; + let channel_permission: Vec = load_or_empty( + channel_permissions + .filter(channel_uuid.eq(self.uuid)) + .select(ChannelPermission::as_select()) + .load(conn) + .await, + )?; + + Ok(Channel { + uuid: self.uuid, + guild_uuid: self.guild_uuid, + name: self.name, + description: self.description, + permissions: channel_permission, + }) + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Channel { + pub uuid: Uuid, + pub guild_uuid: Uuid, + name: String, + description: Option, + pub permissions: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] +#[diesel(table_name = channel_permissions)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct ChannelPermission { + pub role_uuid: Uuid, + pub permissions: i64, +} + +impl Channel { + pub async fn fetch_all( + pool: &deadpool::managed::Pool< + AsyncDieselConnectionManager, + Conn, + >, + guild_uuid: Uuid, + ) -> Result, Error> { + let mut conn = pool.get().await?; + + use channels::dsl; + let channel_builders: Vec = load_or_empty( + dsl::channels + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(ChannelBuilder::as_select()) + .load(&mut conn) + .await, + )?; + + let channel_futures = channel_builders.iter().map(async move |c| { + let mut conn = pool.get().await?; + c.clone().build(&mut conn).await + }); + + futures::future::try_join_all(channel_futures).await + } + + pub async fn fetch_one(conn: &mut Conn, channel_uuid: Uuid) -> Result { + use channels::dsl; + let channel_builder: ChannelBuilder = dsl::channels + .filter(dsl::uuid.eq(channel_uuid)) + .select(ChannelBuilder::as_select()) + .get_result(conn) + .await?; + + channel_builder.build(conn).await + } + + pub async fn new( + data: actix_web::web::Data, + guild_uuid: Uuid, + name: String, + description: Option, + ) -> Result { + let mut conn = data.pool.get().await?; + + let channel_uuid = Uuid::now_v7(); + + let new_channel = ChannelBuilder { + uuid: channel_uuid, + guild_uuid, + name: name.clone(), + description: description.clone(), + }; + + insert_into(channels::table) + .values(new_channel) + .execute(&mut conn) + .await?; + + // returns different object because there's no reason to build the channelbuilder (wastes 1 database request) + let channel = Self { + uuid: channel_uuid, + guild_uuid, + name, + description, + permissions: vec![], + }; + + data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) + .await?; + + data.del_cache_key(format!("{}_channels", guild_uuid)) + .await?; + + Ok(channel) + } + + pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { + use channels::dsl; + delete(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .execute(conn) + .await?; + + Ok(()) + } + + pub async fn fetch_messages( + &self, + conn: &mut Conn, + amount: i64, + offset: i64, + ) -> Result, Error> { + use messages::dsl; + let messages: Vec = load_or_empty( + dsl::messages + .filter(dsl::channel_uuid.eq(self.uuid)) + .select(Message::as_select()) + .limit(amount) + .offset(offset) + .load(conn) + .await, + )?; + + Ok(messages) + } + + pub async fn new_message( + &self, + conn: &mut Conn, + user_uuid: Uuid, + message: String, + ) -> Result { + let message_uuid = Uuid::now_v7(); + + let message = Message { + uuid: message_uuid, + channel_uuid: self.uuid, + user_uuid, + message, + }; + + insert_into(messages::table) + .values(message.clone()) + .execute(conn) + .await?; + + Ok(message) + } +} + +#[derive(Clone, Copy)] +pub enum Permissions { + SendMessage = 1, + CreateChannel = 2, + DeleteChannel = 4, + ManageChannel = 8, + CreateRole = 16, + DeleteRole = 32, + ManageRole = 64, + CreateInvite = 128, + ManageInvite = 256, + ManageServer = 512, + ManageMember = 1024, +} + +impl Permissions { + pub fn fetch_permissions(permissions: i64) -> Vec { + let all_perms = vec![ + Self::SendMessage, + Self::CreateChannel, + Self::DeleteChannel, + Self::ManageChannel, + Self::CreateRole, + Self::DeleteRole, + Self::ManageRole, + Self::CreateInvite, + Self::ManageInvite, + Self::ManageServer, + Self::ManageMember, + ]; + + all_perms + .into_iter() + .filter(|p| permissions & (*p as i64) != 0) + .collect() + } +} + +#[derive(Serialize, Queryable, Selectable, Insertable, Clone)] +#[diesel(table_name = guilds)] +#[diesel(check_for_backend(diesel::pg::Pg))] +struct GuildBuilder { + uuid: Uuid, + name: String, + description: Option, + icon: Option, + owner_uuid: Uuid, +} + +impl GuildBuilder { + async fn build(self, conn: &mut Conn) -> Result { + let member_count = Member::count(conn, self.uuid).await?; + + let roles = Role::fetch_all(conn, self.uuid).await?; + + Ok(Guild { + uuid: self.uuid, + name: self.name, + description: self.description, + icon: self.icon.and_then(|i| i.parse().ok()), + owner_uuid: self.owner_uuid, + roles, + member_count, + }) + } +} + +#[derive(Serialize)] +pub struct Guild { + pub uuid: Uuid, + name: String, + description: Option, + icon: Option, + owner_uuid: Uuid, + pub roles: Vec, + member_count: i64, +} + +impl Guild { + pub async fn fetch_one(conn: &mut Conn, guild_uuid: Uuid) -> Result { + use guilds::dsl; + let guild_builder: GuildBuilder = dsl::guilds + .filter(dsl::uuid.eq(guild_uuid)) + .select(GuildBuilder::as_select()) + .get_result(conn) + .await?; + + guild_builder.build(conn).await + } + + pub async fn fetch_amount( + pool: &deadpool::managed::Pool< + AsyncDieselConnectionManager, + Conn, + >, + offset: i64, + amount: i64, + ) -> Result, Error> { + // Fetch guild data from database + let mut conn = pool.get().await?; + + use guilds::dsl; + let guild_builders: Vec = load_or_empty( + dsl::guilds + .select(GuildBuilder::as_select()) + .order_by(dsl::uuid) + .offset(offset) + .limit(amount) + .load(&mut conn) + .await, + )?; + + // Process each guild concurrently + let guild_futures = guild_builders.iter().map(async move |g| { + let mut conn = pool.get().await?; + g.clone().build(&mut conn).await + }); + + // Execute all futures concurrently and collect results + futures::future::try_join_all(guild_futures).await + } + + pub async fn new( + conn: &mut Conn, + name: String, + description: Option, + owner_uuid: Uuid, + ) -> Result { + let guild_uuid = Uuid::now_v7(); + + let guild_builder = GuildBuilder { + uuid: guild_uuid, + name: name.clone(), + description: description.clone(), + icon: None, + owner_uuid, + }; + + insert_into(guilds::table) + .values(guild_builder) + .execute(conn) + .await?; + + let member_uuid = Uuid::now_v7(); + + let member = Member { + uuid: member_uuid, + nickname: None, + user_uuid: owner_uuid, + guild_uuid, + }; + + insert_into(guild_members::table) + .values(member) + .execute(conn) + .await?; + + Ok(Guild { + uuid: guild_uuid, + name, + description, + icon: None, + owner_uuid, + roles: vec![], + member_count: 1, + }) + } + + pub async fn get_invites(&self, conn: &mut Conn) -> Result, Error> { + use invites::dsl; + let invites = load_or_empty( + dsl::invites + .filter(dsl::guild_uuid.eq(self.uuid)) + .select(Invite::as_select()) + .load(conn) + .await, + )?; + + Ok(invites) + } + + pub async fn create_invite( + &self, + conn: &mut Conn, + member: &Member, + custom_id: Option, + ) -> Result { + let invite_id; + + if let Some(id) = custom_id { + invite_id = id; + if invite_id.len() > 32 { + return Err(Error::BadRequest("MAX LENGTH".to_string())); + } + } else { + let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + invite_id = random_string::generate(8, charset); + } + + let invite = Invite { + id: invite_id, + user_uuid: member.user_uuid, + guild_uuid: self.uuid, + }; + + insert_into(invites::table) + .values(invite.clone()) + .execute(conn) + .await?; + + Ok(invite) + } + + // FIXME: Horrible security + pub async fn set_icon( + &mut self, + storage: &Storage, + conn: &mut Conn, + icon: BytesMut, + ) -> Result<(), Error> { + let icon_clone = icon.clone(); + let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??; + + if let Some(icon) = &self.icon { + let relative_url = icon.path().trim_start_matches('/'); + + storage.delete(relative_url).await?; + } + + let path = format!("icons/{}/icon.{}", self.uuid, image_type); + + let icon_url = storage.write(path.clone(), icon.into()).await?; + + use guilds::dsl; + update(guilds::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::icon.eq(icon_url.as_str())) + .execute(conn) + .await?; + + self.icon = Some(icon_url); + + Ok(()) + } +} + +#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = roles)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Role { + uuid: Uuid, + guild_uuid: Uuid, + name: String, + color: i32, + position: i32, + permissions: i64, +} + +impl Role { + pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { + use roles::dsl; + let roles: Vec = load_or_empty( + dsl::roles + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(Role::as_select()) + .load(conn) + .await, + )?; + + Ok(roles) + } + + pub async fn fetch_one(conn: &mut Conn, role_uuid: Uuid) -> Result { + use roles::dsl; + let role: Role = dsl::roles + .filter(dsl::uuid.eq(role_uuid)) + .select(Role::as_select()) + .get_result(conn) + .await?; + + Ok(role) + } + + pub async fn new(conn: &mut Conn, guild_uuid: Uuid, name: String) -> Result { + let role_uuid = Uuid::now_v7(); + + let role = Role { + uuid: role_uuid, + guild_uuid, + name, + color: 16777215, + position: 0, + permissions: 0, + }; + + insert_into(roles::table) + .values(role.clone()) + .execute(conn) + .await?; + + Ok(role) + } +} + +#[derive(Queryable, Selectable, Insertable)] +#[diesel(table_name = guild_members)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Member { + pub uuid: Uuid, + pub nickname: Option, + pub user_uuid: Uuid, + pub guild_uuid: Uuid, +} + +impl Member { + async fn count(conn: &mut Conn, guild_uuid: Uuid) -> Result { + use guild_members::dsl; + let count: i64 = dsl::guild_members + .filter(dsl::guild_uuid.eq(guild_uuid)) + .count() + .get_result(conn) + .await?; + + Ok(count) + } + + pub async fn fetch_one( + conn: &mut Conn, + user_uuid: Uuid, + guild_uuid: Uuid, + ) -> Result { + use guild_members::dsl; + let member: Member = dsl::guild_members + .filter(dsl::user_uuid.eq(user_uuid)) + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(Member::as_select()) + .get_result(conn) + .await?; + + Ok(member) + } + + pub async fn new(conn: &mut Conn, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + let member_uuid = Uuid::now_v7(); + + let member = Member { + uuid: member_uuid, + guild_uuid, + user_uuid, + nickname: None, + }; + + insert_into(guild_members::table) + .values(member) + .execute(conn) + .await?; + + Ok(Self { + uuid: member_uuid, + nickname: None, + user_uuid, + guild_uuid, + }) + } +} + +#[derive(Clone, Serialize, Queryable, Selectable, Insertable)] +#[diesel(table_name = messages)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Message { + uuid: Uuid, + channel_uuid: Uuid, + user_uuid: Uuid, + message: String, +} + +/// Server invite struct +#[derive(Clone, Serialize, Queryable, Selectable, Insertable)] +pub struct Invite { + /// case-sensitive alphanumeric string with a fixed length of 8 characters, can be up to 32 characters for custom invites + id: String, + /// User that created the invite + user_uuid: Uuid, + /// UUID of the guild that the invite belongs to + pub guild_uuid: Uuid, +} + +impl Invite { + pub async fn fetch_one(conn: &mut Conn, invite_id: String) -> Result { + use invites::dsl; + let invite: Invite = dsl::invites + .filter(dsl::id.eq(invite_id)) + .select(Invite::as_select()) + .get_result(conn) + .await?; + + Ok(invite) + } +} + +#[derive(Serialize, Clone, Queryable, Selectable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct User { + uuid: Uuid, + username: String, + display_name: Option, + avatar: Option, +} + +impl User { + pub async fn fetch_one(conn: &mut Conn, user_uuid: Uuid) -> Result { + use users::dsl; + let user: User = dsl::users + .filter(dsl::uuid.eq(user_uuid)) + .select(User::as_select()) + .get_result(conn) + .await?; + + Ok(user) + } + + pub async fn fetch_amount( + conn: &mut Conn, + offset: i64, + amount: i64, + ) -> Result, Error> { + use users::dsl; + let users: Vec = load_or_empty( + dsl::users + .limit(amount) + .offset(offset) + .select(User::as_select()) + .load(conn) + .await, + )?; + + Ok(users) + } +} + +#[derive(Serialize, Queryable, Selectable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Me { + uuid: Uuid, + username: String, + display_name: Option, + avatar: Option, + email: String, + email_verified: bool, +} + +impl Me { + pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { + use users::dsl; + let me: Me = dsl::users + .filter(dsl::uuid.eq(user_uuid)) + .select(Me::as_select()) + .get_result(conn) + .await?; + + Ok(me) + } + + pub async fn set_avatar( + &mut self, + storage: &Storage, + conn: &mut Conn, + avatar: BytesMut, + ) -> Result<(), Error> { + let avatar_clone = avatar.clone(); + let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; + + if let Some(avatar) = &self.avatar { + let avatar_url: Url = avatar.parse()?; + + let relative_url = avatar_url.path().trim_start_matches('/'); + + storage.delete(relative_url).await?; + } + + let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); + + let avatar_url = storage + .write(path.clone(), avatar.into()) + .await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::avatar.eq(avatar_url.as_str())) + .execute(conn) + .await?; + + self.avatar = Some(avatar_url.to_string()); + + Ok(()) + } +} + +#[derive(Deserialize)] +pub struct StartAmountQuery { + pub start: Option, + pub amount: Option, +} diff --git a/src/utils.rs b/src/utils.rs index 7ef880e..f9a5705 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,56 +1,73 @@ -use rand::seq::IndexedRandom; -use std::sync::LazyLock; - -use axum::body::Bytes; -use axum_extra::extract::cookie::{Cookie, SameSite}; +use actix_web::{ + cookie::{Cookie, SameSite, time::Duration}, + http::header::HeaderMap, + web::BytesMut, +}; use bindet::FileType; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; use getrandom::fill; use hex::encode; -use regex::Regex; -use serde::{Serialize, de::DeserializeOwned}; -use time::Duration; -use uuid::Uuid; +use redis::RedisError; +use serde::Serialize; -use crate::{ - Conn, - config::Config, - error::Error, - objects::{HasIsAbove, HasUuid}, - schema::users, - wordlist::{ADJECTIVES, ANIMALS}, -}; +use crate::{Data, error::Error}; -pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(r"[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+(?:\.[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+)*@(?:[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?").unwrap() -}); +pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, Error> { + let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); -pub static USERNAME_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); + if auth_token.is_none() { + return Err(Error::Unauthorized( + "No authorization header provided".to_string(), + )); + } -pub static CHANNEL_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); + let auth_raw = auth_token.unwrap().to_str()?; -pub static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); + let mut auth = auth_raw.split_whitespace(); -pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cookie { - Cookie::build(("refresh_token", refresh_token)) + let auth_type = auth.next(); + + let auth_value = auth.next(); + + if auth_type.is_none() { + return Err(Error::BadRequest( + "Authorization header is empty".to_string(), + )); + } else if auth_type.is_some_and(|at| at != "Bearer") { + return Err(Error::BadRequest( + "Only token auth is supported".to_string(), + )); + } + + if auth_value.is_none() { + return Err(Error::BadRequest("No token provided".to_string())); + } + + Ok(auth_value.unwrap()) +} + +pub fn refresh_token_cookie(refresh_token: String) -> Cookie<'static> { + Cookie::build("refresh_token", refresh_token) .http_only(true) .secure(true) .same_site(SameSite::None) - .path(config.web.backend_url.path().to_string()) + .path("/api") .max_age(Duration::days(30)) - .build() + .finish() } -pub fn generate_token() -> Result { - let mut buf = [0u8; N]; +pub fn generate_access_token() -> Result { + let mut buf = [0u8; 16]; fill(&mut buf)?; Ok(encode(buf)) } -pub fn image_check(icon: Bytes) -> Result { +pub fn generate_refresh_token() -> Result { + let mut buf = [0u8; 32]; + fill(&mut buf)?; + Ok(encode(buf)) +} + +pub fn image_check(icon: BytesMut) -> Result { let buf = std::io::Cursor::new(icon); let detect = bindet::detect(buf).map_err(|e| e.kind()); @@ -68,118 +85,14 @@ pub fn image_check(icon: Bytes) -> Result { )) } -pub async fn user_uuid_from_identifier( - conn: &mut Conn, - identifier: &String, -) -> Result { - if EMAIL_REGEX.is_match(identifier) { - use users::dsl; - let user_uuid = dsl::users - .filter(dsl::email.eq(identifier)) - .select(dsl::uuid) - .get_result(conn) - .await?; - - Ok(user_uuid) - } else if USERNAME_REGEX.is_match(identifier) { - use users::dsl; - let user_uuid = dsl::users - .filter(dsl::username.eq(identifier)) - .select(dsl::uuid) - .get_result(conn) - .await?; - - Ok(user_uuid) - } else { - Err(Error::BadRequest( - "Please provide a valid username or email".to_string(), - )) - } -} - -pub async fn user_uuid_from_username(conn: &mut Conn, username: &String) -> Result { - if USERNAME_REGEX.is_match(username) { - use users::dsl; - let user_uuid = dsl::users - .filter(dsl::username.eq(username)) - .select(dsl::uuid) - .get_result(conn) - .await?; - - Ok(user_uuid) - } else { - Err(Error::BadRequest( - "Please provide a valid username".to_string(), - )) - } -} - -pub async fn global_checks(conn: &mut Conn, config: &Config, user_uuid: Uuid) -> Result<(), Error> { - if config.instance.require_email_verification { - use users::dsl; - let email_verified: bool = dsl::users - .filter(dsl::uuid.eq(user_uuid)) - .select(dsl::email_verified) - .get_result(conn) - .await?; - - if !email_verified { - return Err(Error::Forbidden( - "server requires email verification".to_string(), - )); - } - } - - Ok(()) -} - -pub async fn order_by_is_above(mut items: Vec) -> Result, Error> -where - T: HasUuid + HasIsAbove, -{ - let mut ordered = Vec::new(); - - // Find head - let head_pos = items - .iter() - .position(|item| !items.iter().any(|i| i.is_above() == Some(item.uuid()))); - - if let Some(pos) = head_pos { - ordered.push(items.swap_remove(pos)); - - while let Some(next_pos) = items - .iter() - .position(|item| Some(item.uuid()) == ordered.last().unwrap().is_above()) - { - ordered.push(items.swap_remove(next_pos)); - } - } - - Ok(ordered) -} - -#[allow(async_fn_in_trait)] -pub trait CacheFns { - async fn set_cache_key( - &self, - key: String, - value: impl Serialize, - expire: u32, - ) -> Result<(), Error>; - async fn get_cache_key(&self, key: String) -> Result - where - T: DeserializeOwned; - async fn del_cache_key(&self, key: String) -> Result<(), Error>; -} - -impl CacheFns for redis::Client { - async fn set_cache_key( +impl Data { + pub async fn set_cache_key( &self, key: String, value: impl Serialize, expire: u32, ) -> Result<(), Error> { - let mut conn = self.get_multiplexed_tokio_connection().await?; + let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; let key_encoded = encode(key); @@ -198,39 +111,25 @@ impl CacheFns for redis::Client { Ok(()) } - async fn get_cache_key(&self, key: String) -> Result - where - T: DeserializeOwned, - { - let mut conn = self.get_multiplexed_tokio_connection().await?; + pub async fn get_cache_key(&self, key: String) -> Result { + let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; let key_encoded = encode(key); - let res: String = redis::cmd("GET") + redis::cmd("GET") .arg(key_encoded) .query_async(&mut conn) - .await?; - - Ok(serde_json::from_str(&res)?) + .await } - async fn del_cache_key(&self, key: String) -> Result<(), Error> { - let mut conn = self.get_multiplexed_tokio_connection().await?; + pub async fn del_cache_key(&self, key: String) -> Result<(), RedisError> { + let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; let key_encoded = encode(key); - Ok(redis::cmd("DEL") + redis::cmd("DEL") .arg(key_encoded) .query_async(&mut conn) - .await?) + .await } } - -pub fn generate_device_name() -> String { - let mut rng = rand::rng(); - - let adjective = ADJECTIVES.choose(&mut rng).unwrap(); - let animal = ANIMALS.choose(&mut rng).unwrap(); - - [*adjective, *animal].join(" ") -} diff --git a/src/wordlist.rs b/src/wordlist.rs deleted file mode 100644 index 1227c1f..0000000 --- a/src/wordlist.rs +++ /dev/null @@ -1,993 +0,0 @@ -pub const ANIMALS: [&str; 223] = [ - "Aardvark", - "Albatross", - "Alligator", - "Alpaca", - "Ant", - "Anteater", - "Antelope", - "Ape", - "Armadillo", - "Donkey", - "Baboon", - "Badger", - "Barracuda", - "Bat", - "Bear", - "Beaver", - "Bee", - "Bison", - "Boar", - "Buffalo", - "Butterfly", - "Camel", - "Capybara", - "Caribou", - "Cassowary", - "Cat", - "Caterpillar", - "Cattle", - "Chamois", - "Cheetah", - "Chicken", - "Chimpanzee", - "Chinchilla", - "Chough", - "Clam", - "Cobra", - "Cockroach", - "Cod", - "Cormorant", - "Coyote", - "Crab", - "Crane", - "Crocodile", - "Crow", - "Curlew", - "Deer", - "Dinosaur", - "Dog", - "Dogfish", - "Dolphin", - "Dotterel", - "Dove", - "Dragonfly", - "Duck", - "Dugong", - "Dunlin", - "Eagle", - "Echidna", - "Eel", - "Eland", - "Elephant", - "Elk", - "Emu", - "Falcon", - "Ferret", - "Finch", - "Fish", - "Flamingo", - "Fly", - "Fox", - "Frog", - "Gaur", - "Gazelle", - "Gerbil", - "Giraffe", - "Gnat", - "Gnu", - "Goat", - "Goldfinch", - "Goldfish", - "Goose", - "Gorilla", - "Goshawk", - "Grasshopper", - "Grouse", - "Guanaco", - "Gull", - "Hamster", - "Hare", - "Hawk", - "Hedgehog", - "Heron", - "Herring", - "Hippopotamus", - "Hornet", - "Horse", - "Hummingbird", - "Hyena", - "Ibex", - "Ibis", - "Jackal", - "Jaguar", - "Jay", - "Jellyfish", - "Kangaroo", - "Kingfisher", - "Koala", - "Kookabura", - "Kouprey", - "Kudu", - "Lapwing", - "Lark", - "Lemur", - "Leopard", - "Lion", - "Llama", - "Lobster", - "Locust", - "Loris", - "Louse", - "Lyrebird", - "Magpie", - "Mallard", - "Manatee", - "Mandrill", - "Mantis", - "Marten", - "Meerkat", - "Mink", - "Mole", - "Mongoose", - "Monkey", - "Moose", - "Mosquito", - "Mouse", - "Mule", - "Narwhal", - "Newt", - "Nightingale", - "Octopus", - "Okapi", - "Opossum", - "Oryx", - "Ostrich", - "Otter", - "Owl", - "Oyster", - "Panther", - "Parrot", - "Partridge", - "Peafowl", - "Pelican", - "Penguin", - "Pheasant", - "Pig", - "Pigeon", - "Pony", - "Porcupine", - "Porpoise", - "Quail", - "Quelea", - "Quetzal", - "Rabbit", - "Raccoon", - "Rail", - "Ram", - "Rat", - "Raven", - "Red Deer", - "Red Panda", - "Reindeer", - "Rhinoceros", - "Rook", - "Salamander", - "Salmon", - "Sand Dollar", - "Sandpiper", - "Sardine", - "Scorpion", - "Seahorse", - "Seal", - "Shark", - "Sheep", - "Shrew", - "Skunk", - "Snail", - "Snake", - "Sparrow", - "Spider", - "Spoonbill", - "Squid", - "Squirrel", - "Starling", - "Stingray", - "Stinkbug", - "Stork", - "Swallow", - "Swan", - "Tapir", - "Tarsier", - "Termite", - "Tiger", - "Toad", - "Trout", - "Turkey", - "Turtle", - "Viper", - "Vulture", - "Wallaby", - "Walrus", - "Wasp", - "Weasel", - "Whale", - "Wildcat", - "Wolf", - "Wolverine", - "Wombat", - "Woodcock", - "Woodpecker", - "Worm", - "Wren", - "Yak", - "Zebra", -]; - -pub const ADJECTIVES: [&str; 765] = [ - "Other", - "Such", - "First", - "Many", - "New", - "More", - "Same", - "Own", - "Good", - "Different", - "Great", - "Long", - "High", - "Social", - "Little", - "Much", - "Important", - "Small", - "Most", - "Large", - "Old", - "Few", - "General", - "Second", - "Public", - "Last", - "Several", - "Early", - "Certain", - "Economic", - "Least", - "Common", - "Present", - "Next", - "Local", - "Best", - "Particular", - "Young", - "Various", - "Necessary", - "Whole", - "Only", - "True", - "Able", - "Major", - "Full", - "Low", - "Available", - "Real", - "Similar", - "Total", - "Special", - "Less", - "Short", - "Specific", - "Single", - "Self", - "National", - "Individual", - "Clear", - "Personal", - "Higher", - "Better", - "Third", - "Natural", - "Greater", - "Open", - "Difficult", - "Current", - "Further", - "Main", - "Physical", - "Foreign", - "Lower", - "Strong", - "Private", - "Likely", - "International", - "Significant", - "Late", - "Basic", - "Hard", - "Modern", - "Simple", - "Normal", - "Sure", - "Central", - "Original", - "Effective", - "Following", - "Direct", - "Final", - "Cultural", - "Big", - "Recent", - "Complete", - "Financial", - "Positive", - "Primary", - "Appropriate", - "Legal", - "European", - "Equal", - "Larger", - "Average", - "Historical", - "Critical", - "Wide", - "Traditional", - "Additional", - "Active", - "Complex", - "Former", - "Independent", - "Entire", - "Actual", - "Close", - "Constant", - "Previous", - "Easy", - "Serious", - "Potential", - "Fine", - "Industrial", - "Subject", - "Future", - "Internal", - "Initial", - "Well", - "Essential", - "Dark", - "Popular", - "Successful", - "Standard", - "Year", - "Past", - "Ready", - "Professional", - "Wrong", - "Very", - "Proper", - "Separate", - "Heavy", - "Civil", - "Responsible", - "Considerable", - "Light", - "Cold", - "Above", - "Older", - "Practical", - "External", - "Sufficient", - "Interesting", - "Upper", - "Scientific", - "Key", - "Annual", - "Limited", - "Smaller", - "Southern", - "Earlier", - "Commercial", - "Powerful", - "Later", - "Like", - "Clinical", - "Ancient", - "Educational", - "Typical", - "Technical", - "Environmental", - "Formal", - "Aware", - "Beautiful", - "Variable", - "Obvious", - "Secondary", - "Enough", - "Urban", - "Regular", - "Relevant", - "Greatest", - "Spiritual", - "Time", - "Double", - "Happy", - "Term", - "Multiple", - "Dependent", - "Correct", - "Northern", - "Middle", - "Rural", - "Official", - "Fundamental", - "Numerous", - "Overall", - "Usual", - "Native", - "Regional", - "Highest", - "North", - "Agricultural", - "Literary", - "Broad", - "Perfect", - "Experimental", - "Fourth", - "Global", - "Ordinary", - "Related", - "Apparent", - "Daily", - "Principal", - "Contemporary", - "Severe", - "Reasonable", - "Subsequent", - "Worth", - "Longer", - "Emotional", - "Intellectual", - "Unique", - "Pure", - "Familiar", - "American", - "Solid", - "Brief", - "Famous", - "Fresh", - "Day", - "Corresponding", - "Characteristic", - "Maximum", - "Detailed", - "Outside", - "Theoretical", - "Fair", - "Opposite", - "Capable", - "Visual", - "Interested", - "Joint", - "Adequate", - "Based", - "Substantial", - "Unable", - "Structural", - "Soft", - "False", - "Largest", - "Inner", - "Mean", - "Extensive", - "Excellent", - "Rapid", - "Absolute", - "Consistent", - "Continuous", - "Administrative", - "Strange", - "Willing", - "Alternative", - "Slow", - "Distinct", - "Safe", - "Permanent", - "Front", - "Corporate", - "Academic", - "Thin", - "Nineteenth", - "Universal", - "Functional", - "Unknown", - "Careful", - "Narrow", - "Evident", - "Sound", - "Classical", - "Minor", - "Weak", - "Suitable", - "Chief", - "Extreme", - "Yellow", - "Warm", - "Mixed", - "Flat", - "Huge", - "Vast", - "Stable", - "Valuable", - "Rare", - "Visible", - "Sensitive", - "Mechanical", - "State", - "Radical", - "Extra", - "Superior", - "Conventional", - "Thick", - "Dominant", - "Post", - "Collective", - "Younger", - "Efficient", - "Linear", - "Organic", - "Oral", - "Century", - "Creative", - "Vertical", - "Dynamic", - "Empty", - "Minimum", - "Cognitive", - "Logical", - "Afraid", - "Equivalent", - "Quick", - "Near", - "Concrete", - "Mass", - "Acute", - "Sharp", - "Easier", - "Quiet", - "Adult", - "Accurate", - "Ideal", - "Partial", - "Bright", - "Identical", - "Conservative", - "Magnetic", - "Frequent", - "Electronic", - "Fixed", - "Square", - "Cross", - "Clean", - "Back", - "Organizational", - "Constitutional", - "Genetic", - "Ultimate", - "Secret", - "Vital", - "Dramatic", - "Objective", - "Round", - "Alive", - "Straight", - "Unusual", - "Rational", - "Electric", - "Mutual", - "Class", - "Competitive", - "Revolutionary", - "Statistical", - "Random", - "Musical", - "Crucial", - "Racial", - "Sudden", - "Acid", - "Content", - "Temporary", - "Line", - "Remarkable", - "Exact", - "Valid", - "Helpful", - "Nice", - "Comprehensive", - "United", - "Level", - "Fifth", - "Nervous", - "Expensive", - "Prominent", - "Healthy", - "Liquid", - "Institutional", - "Silent", - "Sweet", - "Strategic", - "Molecular", - "Comparative", - "Called", - "Electrical", - "Raw", - "Acceptable", - "Scale", - "Violent", - "All", - "Desirable", - "Tall", - "Steady", - "Wonderful", - "Sub", - "Distant", - "Progressive", - "Enormous", - "Horizontal", - "And", - "Intense", - "Smooth", - "Applicable", - "Over", - "Animal", - "Abstract", - "Wise", - "Worst", - "Gold", - "Precise", - "Legislative", - "Remote", - "Technological", - "Outer", - "Uniform", - "Slight", - "Attractive", - "Evil", - "Tiny", - "Royal", - "Angry", - "Advanced", - "Friendly", - "Dear", - "Busy", - "Spatial", - "Rough", - "Primitive", - "Judicial", - "Systematic", - "Lateral", - "Sorry", - "Plain", - "Off", - "Comfortable", - "Definite", - "Massive", - "Firm", - "Widespread", - "Prior", - "Twentieth", - "Mathematical", - "Verbal", - "Marginal", - "Excessive", - "Stronger", - "Gross", - "World", - "Productive", - "Wider", - "Glad", - "Linguistic", - "Patient", - "Symbolic", - "Earliest", - "Plastic", - "Type", - "Prime", - "Eighteenth", - "Blind", - "Neutral", - "Guilty", - "Hand", - "Extraordinary", - "Metal", - "Surprising", - "Fellow", - "York", - "Grand", - "Thermal", - "Artificial", - "Five", - "Lowest", - "Genuine", - "Dimensional", - "Optical", - "Unlikely", - "Developmental", - "Reliable", - "Executive", - "Comparable", - "Satisfactory", - "Golden", - "Diverse", - "Preliminary", - "Wooden", - "Noble", - "Part", - "Striking", - "Cool", - "Classic", - "Elderly", - "Four", - "Temporal", - "Indirect", - "Romantic", - "Intermediate", - "Differential", - "Passive", - "Life", - "Voluntary", - "Out", - "Adjacent", - "Behavioral", - "Exclusive", - "Closed", - "Inherent", - "Inevitable", - "Complicated", - "Quantitative", - "Respective", - "Artistic", - "Probable", - "Anxious", - "Informal", - "Strict", - "Fiscal", - "Ideological", - "Profound", - "Extended", - "Eternal", - "Known", - "Infinite", - "Proud", - "Honest", - "Peculiar", - "Absent", - "Pleasant", - "Optimal", - "Renal", - "Static", - "Outstanding", - "Presidential", - "Digital", - "Integrated", - "Legitimate", - "Curious", - "Aggressive", - "Deeper", - "Elementary", - "History", - "Surgical", - "Occasional", - "Flexible", - "Convenient", - "Solar", - "Atomic", - "Isolated", - "Latest", - "Sad", - "Conceptual", - "Underlying", - "Everyday", - "Cost", - "Intensive", - "Odd", - "Subjective", - "Mid", - "Worthy", - "Pale", - "Meaningful", - "Therapeutic", - "Making", - "Circular", - "Realistic", - "Multi", - "Child", - "Sophisticated", - "Down", - "Leading", - "Intelligent", - "Governmental", - "Numerical", - "Minimal", - "Diagnostic", - "Indigenous", - "Aesthetic", - "Distinctive", - "Operational", - "Sole", - "Material", - "Fast", - "Bitter", - "Broader", - "Brilliant", - "Peripheral", - "Rigid", - "Automatic", - "Lesser", - "Routine", - "Favorable", - "Cooperative", - "Cardiac", - "Arbitrary", - "Loose", - "Favorite", - "Subtle", - "Uncertain", - "Hostile", - "Monthly", - "Naval", - "Physiological", - "Historic", - "Developed", - "Skilled", - "Anterior", - "Pro", - "Gentle", - "Loud", - "Pulmonary", - "Innocent", - "Provincial", - "Mild", - "Page", - "Specialized", - "Bare", - "Excess", - "Inter", - "Shaped", - "Theological", - "Sensory", - "The", - "Stress", - "Novel", - "Working", - "Shorter", - "Secular", - "Geographical", - "Intimate", - "Liable", - "Selective", - "Influential", - "Modest", - "Successive", - "Continued", - "Water", - "Expert", - "Municipal", - "Marine", - "Thirty", - "Adverse", - "Wacky", - "Closer", - "Virtual", - "Peaceful", - "Mobile", - "Sixth", - "Immune", - "Coastal", - "Representative", - "Lead", - "Forward", - "Faithful", - "Crystal", - "Protective", - "Elaborate", - "Tremendous", - "Welcoming", - "Abnormal", - "Grateful", - "Proportional", - "Dual", - "Operative", - "Precious", - "Sympathetic", - "Accessible", - "Lovely", - "Spinal", - "Even", - "Marked", - "Observed", - "Point", - "Mature", - "Competent", - "Residential", - "Impressive", - "Unexpected", - "Nearby", - "Unnecessary", - "Generous", - "Cerebral", - "Unpublished", - "Delicate", - "Analytical", - "Tropical", - "Statutory", - "Cell", - "Weekly", - "End", - "Online", - "Beneficial", - "Aged", - "Tough", - "Eager", - "Ongoing", - "Silver", - "Persistent", - "Calm", - "Nearest", - "Hidden", - "Magic", - "Pretty", - "Wealthy", - "Exciting", - "Decisive", - "Confident", - "Invisible", - "Notable", - "Medium", - "Manual", - "Select", - "Thorough", - "Causal", - "Giant", - "Bigger", - "Pink", - "Improved", - "Immense", - "Hour", - "Intact", - "Grade", - "Dense", - "Hungry", - "Biggest", - "Abundant", - "Handsome", - "Retail", - "Insufficient", - "Irregular", - "Intrinsic", - "Residual", - "Follow", - "Fluid", - "Mysterious", - "Descriptive", - "Elastic", - "Destructive", - "Architectural", - "Synthetic", - "Continental", - "Evolutionary", - "Lucky", - "Bold", - "Funny", - "Peak", - "Smallest", - "Reluctant", - "Suspicious", - "Smart", - "Mighty", - "Brave", - "Humble", - "Vocal", - "Obscure", - "Innovative", -];