Compare commits

..

3 commits

113 changed files with 2568 additions and 4383 deletions

View file

@ -3,21 +3,6 @@ when:
branch: main branch: main
steps: steps:
- name: build-x86_64
image: rust:bookworm
commands:
- cargo build --release
- name: build-arm64
image: rust: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
- name: container-build-and-publish - name: container-build-and-publish
image: docker image: docker
commands: commands:

View file

@ -1,19 +0,0 @@
when:
- event: push
branch: main
steps:
- name: build-docs
image: rust: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

View file

@ -8,12 +8,6 @@ strip = true
lto = true lto = true
codegen-units = 1 codegen-units = 1
# Speed up compilation to make dev bearable
[profile.dev]
debug = 0
strip = "debuginfo"
codegen-units = 512
[dependencies] [dependencies]
actix-cors = "0.7.1" actix-cors = "0.7.1"
actix-web = "4.11" actix-web = "4.11"
@ -27,25 +21,16 @@ regex = "1.11"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
simple_logger = "5.0.0" simple_logger = "5.0.0"
redis = { version = "0.32", features= ["tokio-comp"] } sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "postgres"] }
tokio-tungstenite = { version = "0.27", features = ["native-tls", "url"] } redis = { version = "0.31.0", features= ["tokio-comp"] }
tokio-tungstenite = { version = "0.26", features = ["native-tls", "url"] }
toml = "0.8" toml = "0.8"
url = { version = "2.5", features = ["serde"] } url = { version = "2.5", features = ["serde"] }
uuid = { version = "1.17", features = ["serde", "v7"] } uuid = { version = "1.16", features = ["serde", "v7"] }
random-string = "1.1" random-string = "1.1"
actix-ws = "0.3.0" actix-ws = "0.3.0"
futures-util = "0.3.31" futures-util = "0.3.31"
bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false }
bindet = "0.3.2"
deadpool = "0.12"
diesel = { version = "2.2", features = ["uuid", "chrono"], default-features = false }
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"
lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] }
chrono = { version = "0.4.41", features = ["serde"] }
[dependencies.tokio] [dependencies.tokio]
version = "1.45" version = "1.44"
features = ["full"] features = ["full"]

View file

@ -1,17 +1,16 @@
FROM --platform=linux/amd64 debian:12-slim AS prep FROM rust:bookworm AS builder
WORKDIR /src WORKDIR /src
COPY target/release/backend backend-amd64 COPY . .
COPY target/aarch64-unknown-linux-gnu/release/backend backend-arm64
RUN cargo build --release
FROM debian:12-slim 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=builder /src/target/release/backend /usr/bin/gorb-backend
COPY --from=prep /src/backend-${TARGETARCH} /usr/bin/gorb-backend
COPY entrypoint.sh /usr/bin/entrypoint.sh COPY entrypoint.sh /usr/bin/entrypoint.sh
@ -19,23 +18,12 @@ RUN useradd --create-home --home-dir /gorb gorb
USER gorb USER gorb
ENV WEB_FRONTEND_URL=https://gorb.app/web/ \ ENV DATABASE_USERNAME="gorb" \
WEB_BASE_PATH=/api \ DATABASE_PASSWORD="gorb" \
DATABASE_USERNAME=gorb \ DATABASE="gorb" \
DATABASE_PASSWORD=gorb \ DATABASE_HOST="database" \
DATABASE=gorb \ DATABASE_PORT="5432" \
DATABASE_HOST=database \ CACHE_DB_HOST="valkey" \
DATABASE_PORT=5432 \ CACHE_DB_PORT="6379"
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
ENTRYPOINT ["/usr/bin/entrypoint.sh"] ENTRYPOINT ["/usr/bin/entrypoint.sh"]

View file

@ -1,16 +0,0 @@
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);
}

View file

@ -18,21 +18,11 @@ services:
- gorb-backend:/gorb - gorb-backend:/gorb
environment: environment:
#- RUST_LOG=debug #- RUST_LOG=debug
- WEB_FRONTEND_URL=https://gorb.app/web/
- DATABASE_USERNAME=gorb - DATABASE_USERNAME=gorb
- DATABASE_PASSWORD=gorb - DATABASE_PASSWORD=gorb
- DATABASE=gorb - DATABASE=gorb
- DATABASE_HOST=database - DATABASE_HOST=database
- DATABASE_PORT=5432 - 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 <noreply@gorb.app>
- MAIL_TLS=tls
- SMTP_SERVER=mail.gorb.app
- SMTP_USERNAME=your_smtp_username
- SMTP_PASSWORD=your_smtp_password
database: database:
image: postgres:16 image: postgres:16
restart: always restart: always

View file

@ -16,21 +16,11 @@ services:
- gorb-backend:/gorb - gorb-backend:/gorb
environment: environment:
#- RUST_LOG=debug #- RUST_LOG=debug
- WEB_FRONTEND_URL=https://gorb.app/web/
- DATABASE_USERNAME=gorb - DATABASE_USERNAME=gorb
- DATABASE_PASSWORD=gorb - DATABASE_PASSWORD=gorb
- DATABASE=gorb - DATABASE=gorb
- DATABASE_HOST=database - DATABASE_HOST=database
- DATABASE_PORT=5432 - 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 <noreply@gorb.app>
- MAIL_TLS=tls
- SMTP_SERVER=mail.gorb.app
- SMTP_USERNAME=your_smtp_username
- SMTP_PASSWORD=your_smtp_password
database: database:
image: postgres:16 image: postgres:16
restart: always restart: always

View file

@ -1,9 +0,0 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
[migrations_directory]
dir = "migrations"

View file

@ -10,10 +10,6 @@ fi
if [ ! -f "/gorb/config/config.toml" ]; then if [ ! -f "/gorb/config/config.toml" ]; then
cat > /gorb/config/config.toml <<EOF cat > /gorb/config/config.toml <<EOF
[web]
frontend_url = "${WEB_FRONTEND_URL}"
base_path = "${WEB_BASE_PATH}"
[database] [database]
username = "${DATABASE_USERNAME}" username = "${DATABASE_USERNAME}"
password = "${DATABASE_PASSWORD}" password = "${DATABASE_PASSWORD}"
@ -24,22 +20,6 @@ port = ${DATABASE_PORT}
[cache_database] [cache_database]
host = "${CACHE_DB_HOST}" host = "${CACHE_DB_HOST}"
port = ${CACHE_DB_PORT} port = ${CACHE_DB_PORT}
[bunny]
api_key = "${BUNNY_API_KEY}"
endpoint = "${BUNNY_ENDPOINT}"
storage_zone = "${BUNNY_ZONE}"
cdn_url = "${BUNNY_CDN_URL}"
[mail]
address = "${MAIL_ADDRESS}"
tls = "${MAIL_TLS}"
[mail.smtp]
server = "${SMTP_SERVER}"
username = "${SMTP_USERNAME}"
password = "${SMTP_PASSWORD}"
EOF EOF
fi fi
@ -62,7 +42,4 @@ rotate_log() {
rotate_log "/gorb/logs/backend.log" rotate_log "/gorb/logs/backend.log"
# Give the DB time to start up before connecting
sleep 5
/usr/bin/gorb-backend --config /gorb/config/config.toml 2>&1 | tee /gorb/logs/backend.log /usr/bin/gorb-backend --config /gorb/config/config.toml 2>&1 | tee /gorb/logs/backend.log

View file

View file

@ -1,6 +0,0 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View file

@ -1,36 +0,0 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View file

@ -1,4 +0,0 @@
-- This file should undo anything in `up.sql`
DROP INDEX idx_unique_username_active;
DROP INDEX idx_unique_email_active;
DROP TABLE users;

View file

@ -1,20 +0,0 @@
-- Your SQL goes here
CREATE TABLE users (
uuid uuid PRIMARY KEY NOT NULL,
username varchar(32) NOT NULL,
display_name varchar(64) DEFAULT NULL,
password varchar(512) NOT NULL,
email varchar(100) NOT NULL,
email_verified boolean NOT NULL DEFAULT FALSE,
is_deleted boolean NOT NULL DEFAULT FALSE,
deleted_at int8 DEFAULT NULL,
CONSTRAINT unique_username_active UNIQUE NULLS NOT DISTINCT (username, is_deleted),
CONSTRAINT unique_email_active UNIQUE NULLS NOT DISTINCT (email, is_deleted)
);
CREATE UNIQUE INDEX idx_unique_username_active
ON users(username)
WHERE is_deleted = FALSE;
CREATE UNIQUE INDEX idx_unique_email_active
ON users(email)
WHERE is_deleted = FALSE;

View file

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE instance_permissions;

View file

@ -1,5 +0,0 @@
-- Your SQL goes here
CREATE TABLE instance_permissions (
uuid uuid PRIMARY KEY NOT NULL REFERENCES users(uuid),
administrator boolean NOT NULL DEFAULT FALSE
);

View file

@ -1,3 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE access_tokens;
DROP TABLE refresh_tokens;

View file

@ -1,13 +0,0 @@
-- Your SQL goes here
CREATE TABLE refresh_tokens (
token varchar(64) PRIMARY KEY UNIQUE NOT NULL,
uuid uuid NOT NULL REFERENCES users(uuid),
created_at int8 NOT NULL,
device_name varchar(16) NOT NULL
);
CREATE TABLE access_tokens (
token varchar(32) PRIMARY KEY UNIQUE NOT NULL,
refresh_token varchar(64) UNIQUE NOT NULL REFERENCES refresh_tokens(token) ON UPDATE CASCADE ON DELETE CASCADE,
uuid uuid NOT NULL REFERENCES users(uuid),
created_at int8 NOT NULL
);

View file

@ -1,3 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE guild_members;
DROP TABLE guilds;

View file

@ -1,13 +0,0 @@
-- Your SQL goes here
CREATE TABLE guilds (
uuid uuid PRIMARY KEY NOT NULL,
owner_uuid uuid NOT NULL REFERENCES users(uuid),
name VARCHAR(100) NOT NULL,
description VARCHAR(300)
);
CREATE TABLE guild_members (
uuid uuid PRIMARY KEY NOT NULL,
guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE,
user_uuid uuid NOT NULL REFERENCES users(uuid),
nickname VARCHAR(100) DEFAULT NULL
);

View file

@ -1,3 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE role_members;
DROP TABLE roles;

View file

@ -1,15 +0,0 @@
-- Your SQL goes here
CREATE TABLE roles (
uuid uuid UNIQUE NOT NULL,
guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE,
name VARCHAR(50) NOT NULL,
color int NOT NULL DEFAULT 16777215,
position int NOT NULL,
permissions int8 NOT NULL DEFAULT 0,
PRIMARY KEY (uuid, guild_uuid)
);
CREATE TABLE role_members (
role_uuid uuid NOT NULL REFERENCES roles(uuid) ON DELETE CASCADE,
member_uuid uuid NOT NULL REFERENCES guild_members(uuid) ON DELETE CASCADE,
PRIMARY KEY (role_uuid, member_uuid)
);

View file

@ -1,3 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE channel_permissions;
DROP TABLE channels;

View file

@ -1,13 +0,0 @@
-- Your SQL goes here
CREATE TABLE channels (
uuid uuid PRIMARY KEY NOT NULL,
guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE,
name varchar(32) NOT NULL,
description varchar(500) NOT NULL
);
CREATE TABLE channel_permissions (
channel_uuid uuid NOT NULL REFERENCES channels(uuid) ON DELETE CASCADE,
role_uuid uuid NOT NULL REFERENCES roles(uuid) ON DELETE CASCADE,
permissions int8 NOT NULL DEFAULT 0,
PRIMARY KEY (channel_uuid, role_uuid)
);

View file

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE messages;

View file

@ -1,7 +0,0 @@
-- Your SQL goes here
CREATE TABLE messages (
uuid uuid PRIMARY KEY NOT NULL,
channel_uuid uuid NOT NULL REFERENCES channels(uuid) ON DELETE CASCADE,
user_uuid uuid NOT NULL REFERENCES users(uuid),
message varchar(4000) NOT NULL
);

View file

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE invites;

View file

@ -1,6 +0,0 @@
-- Your SQL goes here
CREATE TABLE invites (
id varchar(32) PRIMARY KEY NOT NULL,
guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE,
user_uuid uuid NOT NULL REFERENCES users(uuid)
);

View file

@ -1,4 +0,0 @@
-- This file should undo anything in `up.sql`
UPDATE channels SET description = '' WHERE description IS NULL;
ALTER TABLE ONLY channels ALTER COLUMN description SET NOT NULL;
ALTER TABLE ONLY channels ALTER COLUMN description DROP DEFAULT;

View file

@ -1,3 +0,0 @@
-- Your SQL goes here
ALTER TABLE ONLY channels ALTER COLUMN description DROP NOT NULL;
ALTER TABLE ONLY channels ALTER COLUMN description SET DEFAULT NULL;

View file

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
ALTER TABLE guilds DROP COLUMN icon;

View file

@ -1,2 +0,0 @@
-- Your SQL goes here
ALTER TABLE guilds ADD COLUMN icon VARCHAR(100) DEFAULT NULL;

View file

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
ALTER TABLE users DROP COLUMN avatar;

View file

@ -1,2 +0,0 @@
-- Your SQL goes here
ALTER TABLE users ADD COLUMN avatar varchar(100) DEFAULT NULL;

View file

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
ALTER TABLE channels DROP COLUMN is_above;

View file

@ -1,2 +0,0 @@
-- Your SQL goes here
ALTER TABLE channels ADD COLUMN is_above UUID UNIQUE REFERENCES channels(uuid) DEFAULT NULL;

View file

@ -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;

View file

@ -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;

View file

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE email_tokens;

View file

@ -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)
);

View file

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE password_reset_tokens;

View file

@ -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)
);

View file

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
ALTER TABLE users DROP COLUMN pronouns;

View file

@ -1,2 +0,0 @@
-- Your SQL goes here
ALTER TABLE users ADD COLUMN pronouns VARCHAR(32) DEFAULT NULL;

View file

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
ALTER TABLE users DROP COLUMN about;

View file

@ -1,2 +0,0 @@
-- Your SQL goes here
ALTER TABLE users ADD COLUMN about VARCHAR(200) DEFAULT NULL;

View file

@ -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)
);

View file

@ -1,2 +0,0 @@
-- Your SQL goes here
DROP TABLE email_tokens;

View file

@ -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)
);

View file

@ -1,2 +0,0 @@
-- Your SQL goes here
DROP TABLE password_reset_tokens;

View file

@ -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;

View file

@ -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;

View file

@ -3,7 +3,7 @@
podman-compose --file compose.dev.yml up --build podman-compose --file compose.dev.yml up --build
echo "SHUTTING DOWN CONTAINERS" echo "SHUTTING DOWN CONTAINERS"
podman container stop backend_backend_1 backend_database_1 backend_valkey_1 podman container stop backend_backend_1 backend_database_1
echo "DELETING CONTAINERS" echo "DELETING CONTAINERS"
podman container rm backend_backend_1 backend_database_1 backend_valkey_1 podman container rm backend_backend_1 backend_database_1

View file

@ -1,13 +1,9 @@
//! `/api` Contains the entire API
use actix_web::Scope; use actix_web::Scope;
use actix_web::web; use actix_web::web;
mod v1; mod v1;
mod versions; mod versions;
pub fn web(path: &str) -> Scope { pub fn web() -> Scope {
web::scope(path.trim_end_matches('/')) web::scope("/api").service(v1::web()).service(versions::res)
.service(v1::web())
.service(versions::get)
} }

View file

@ -1,19 +1,14 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{HttpResponse, post, web}; use actix_web::{Error, HttpResponse, post, web};
use argon2::{PasswordHash, PasswordVerifier}; use argon2::{PasswordHash, PasswordVerifier};
use diesel::{ExpressionMethods, QueryDsl, dsl::insert_into}; use log::error;
use diesel_async::RunQueryDsl;
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
Data, Data,
error::Error, api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX},
schema::*, utils::{generate_access_token, generate_refresh_token, refresh_token_cookie},
utils::{
PASSWORD_REGEX, generate_token, new_refresh_token_cookie,
user_uuid_from_identifier,
},
}; };
use super::Response; use super::Response;
@ -34,61 +29,146 @@ pub async fn response(
return Ok(HttpResponse::Forbidden().json(r#"{ "password_hashed": false }"#)); return Ok(HttpResponse::Forbidden().json(r#"{ "password_hashed": false }"#));
} }
use users::dsl; if EMAIL_REGEX.is_match(&login_information.username) {
let row =
sqlx::query_as("SELECT CAST(uuid as VARCHAR), password FROM users WHERE email = $1")
.bind(&login_information.username)
.fetch_one(&data.pool)
.await;
let mut conn = data.pool.get().await?; if let Err(error) = row {
if error.to_string()
== "no rows returned by a query that expected to return at least one row"
{
return Ok(HttpResponse::Unauthorized().finish());
}
let uuid = user_uuid_from_identifier(&mut conn, &login_information.username).await?; error!("{}", error);
return Ok(HttpResponse::InternalServerError().json(
r#"{ "error": "Unhandled exception occured, contact the server administrator" }"#,
));
}
let database_password: String = dsl::users let (uuid, password): (String, String) = row.unwrap();
.filter(dsl::uuid.eq(uuid))
.select(dsl::password)
.get_result(&mut conn)
.await?;
let parsed_hash = PasswordHash::new(&database_password) return Ok(login(
.map_err(|e| Error::PasswordHashError(e.to_string()))?; data.clone(),
uuid,
login_information.password.clone(),
password,
login_information.device_name.clone(),
)
.await);
} else if USERNAME_REGEX.is_match(&login_information.username) {
let row =
sqlx::query_as("SELECT CAST(uuid as VARCHAR), password FROM users WHERE username = $1")
.bind(&login_information.username)
.fetch_one(&data.pool)
.await;
if let Err(error) = row {
if error.to_string()
== "no rows returned by a query that expected to return at least one row"
{
return Ok(HttpResponse::Unauthorized().finish());
}
error!("{}", error);
return Ok(HttpResponse::InternalServerError().json(
r#"{ "error": "Unhandled exception occured, contact the server administrator" }"#,
));
}
let (uuid, password): (String, String) = row.unwrap();
return Ok(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<Data>,
uuid: String,
request_password: String,
database_password: String,
device_name: String,
) -> HttpResponse {
let parsed_hash_raw = PasswordHash::new(&database_password);
if let Err(error) = parsed_hash_raw {
error!("{}", error);
return HttpResponse::InternalServerError().finish();
}
let parsed_hash = parsed_hash_raw.unwrap();
if data if data
.argon2 .argon2
.verify_password(login_information.password.as_bytes(), &parsed_hash) .verify_password(request_password.as_bytes(), &parsed_hash)
.is_err() .is_err()
{ {
return Err(Error::Unauthorized( return HttpResponse::Unauthorized().finish();
"Wrong username or password".to_string(),
));
} }
let refresh_token = generate_token::<32>()?; let refresh_token_raw = generate_refresh_token();
let access_token = generate_token::<16>()?; let access_token_raw = generate_access_token();
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; if let Err(error) = refresh_token_raw {
error!("{}", error);
return HttpResponse::InternalServerError().finish();
}
use refresh_tokens::dsl as rdsl; let refresh_token = refresh_token_raw.unwrap();
insert_into(refresh_tokens::table) if let Err(error) = access_token_raw {
.values(( error!("{}", error);
rdsl::token.eq(&refresh_token), return HttpResponse::InternalServerError().finish();
rdsl::uuid.eq(uuid), }
rdsl::created_at.eq(current_time),
rdsl::device_name.eq(&login_information.device_name),
))
.execute(&mut conn)
.await?;
use access_tokens::dsl as adsl; let access_token = access_token_raw.unwrap();
insert_into(access_tokens::table) let current_time = SystemTime::now()
.values(( .duration_since(UNIX_EPOCH)
adsl::token.eq(&access_token), .unwrap()
adsl::refresh_token.eq(&refresh_token), .as_secs() as i64;
adsl::uuid.eq(uuid),
adsl::created_at.eq(current_time),
))
.execute(&mut conn)
.await?;
Ok(HttpResponse::Ok() if let Err(error) = sqlx::query(&format!(
.cookie(new_refresh_token_cookie(&data.config, refresh_token)) "INSERT INTO refresh_tokens (token, uuid, created_at, device_name) VALUES ($1, '{}', $2, $3 )",
.json(Response { access_token })) uuid
))
.bind(&refresh_token)
.bind(current_time)
.bind(device_name)
.execute(&data.pool)
.await
{
error!("{}", error);
return HttpResponse::InternalServerError().finish();
}
if let Err(error) = sqlx::query(&format!(
"INSERT INTO access_tokens (token, refresh_token, uuid, created_at) VALUES ($1, $2, '{}', $3 )",
uuid
))
.bind(&access_token)
.bind(&refresh_token)
.bind(current_time)
.execute(&data.pool)
.await
{
error!("{}", error);
return HttpResponse::InternalServerError().finish()
}
HttpResponse::Ok()
.cookie(refresh_token_cookie(refresh_token))
.json(Response { access_token })
} }

View file

@ -1,38 +0,0 @@
use actix_web::{HttpRequest, HttpResponse, post, web};
use diesel::{ExpressionMethods, delete};
use diesel_async::RunQueryDsl;
use crate::{
Data,
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)
///
#[post("/logout")]
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized(
"request has no refresh token".to_string(),
))?;
let refresh_token = String::from(refresh_token_cookie.value());
let mut conn = data.pool.get().await?;
delete(refresh_tokens::table)
.filter(dsl::token.eq(refresh_token))
.execute(&mut conn)
.await?;
refresh_token_cookie.make_removal();
Ok(HttpResponse::Ok().cookie(refresh_token_cookie).finish())
}

View file

@ -1,60 +1,79 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::{
str::FromStr,
sync::LazyLock,
time::{SystemTime, UNIX_EPOCH},
};
use actix_web::{Scope, web}; use actix_web::{HttpResponse, Scope, web};
use diesel::{ExpressionMethods, QueryDsl}; use log::error;
use diesel_async::RunQueryDsl; use regex::Regex;
use serde::Serialize; use serde::Serialize;
use sqlx::Postgres;
use uuid::Uuid; use uuid::Uuid;
use crate::{Conn, error::Error, schema::access_tokens::dsl};
mod login; mod login;
mod logout;
mod refresh; mod refresh;
mod register; mod register;
mod reset_password;
mod revoke; mod revoke;
mod verify_email;
#[derive(Serialize)] #[derive(Serialize)]
struct Response { struct Response {
access_token: String, access_token: String,
} }
static EMAIL_REGEX: LazyLock<Regex> = 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()
});
static USERNAME_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap());
// Password is expected to be hashed using SHA3-384
static PASSWORD_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap());
pub fn web() -> Scope { pub fn web() -> Scope {
web::scope("/auth") web::scope("/auth")
.service(register::res) .service(register::res)
.service(login::response) .service(login::response)
.service(logout::res)
.service(refresh::res) .service(refresh::res)
.service(revoke::res) .service(revoke::res)
.service(verify_email::get)
.service(verify_email::post)
.service(reset_password::get)
.service(reset_password::post)
} }
pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result<Uuid, Error> { pub async fn check_access_token(
let (uuid, created_at): (Uuid, i64) = dsl::access_tokens access_token: &str,
.filter(dsl::token.eq(access_token)) pool: &sqlx::Pool<Postgres>,
.select((dsl::uuid, dsl::created_at)) ) -> Result<Uuid, HttpResponse> {
.get_result(conn) let row = sqlx::query_as(
.await "SELECT CAST(uuid as VARCHAR), created_at FROM access_tokens WHERE token = $1",
.map_err(|error| { )
if error == diesel::result::Error::NotFound { .bind(access_token)
Error::Unauthorized("Invalid access token".to_string()) .fetch_one(pool)
} else { .await;
Error::from(error)
}
})?;
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; if let Err(error) = row {
if error.to_string()
== "no rows returned by a query that expected to return at least one row"
{
return Err(HttpResponse::Unauthorized().finish());
}
error!("{}", error);
return Err(HttpResponse::InternalServerError().json(
r#"{ "error": "Unhandled exception occured, contact the server administrator" }"#,
));
}
let (uuid, created_at): (String, i64) = row.unwrap();
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let lifetime = current_time - created_at; let lifetime = current_time - created_at;
if lifetime > 3600 { if lifetime > 3600 {
return Err(Error::Unauthorized("Invalid access token".to_string())); return Err(HttpResponse::Unauthorized().finish());
} }
Ok(uuid) Ok(Uuid::from_str(&uuid).unwrap())
} }

View file

@ -1,50 +1,49 @@
use actix_web::{HttpRequest, HttpResponse, post, web}; use actix_web::{Error, HttpRequest, HttpResponse, post, web};
use diesel::{ExpressionMethods, QueryDsl, delete, update};
use diesel_async::RunQueryDsl;
use log::error; use log::error;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use crate::{ use crate::{
Data, Data,
error::Error, utils::{generate_access_token, generate_refresh_token, refresh_token_cookie},
schema::{
access_tokens::{self, dsl},
refresh_tokens::{self, dsl as rdsl},
},
utils::{generate_token, new_refresh_token_cookie},
}; };
use super::Response; use super::Response;
#[post("/refresh")] #[post("/refresh")]
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( let recv_refresh_token_cookie = req.cookie("refresh_token");
"request has no refresh token".to_string(),
))?;
let mut refresh_token = String::from(refresh_token_cookie.value()); if recv_refresh_token_cookie.is_none() {
return Ok(HttpResponse::Unauthorized().finish());
}
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let mut refresh_token = String::from(recv_refresh_token_cookie.unwrap().value());
let mut conn = data.pool.get().await?; let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
if let Ok(created_at) = rdsl::refresh_tokens if let Ok(row) = sqlx::query_scalar("SELECT created_at FROM refresh_tokens WHERE token = $1")
.filter(rdsl::token.eq(&refresh_token)) .bind(&refresh_token)
.select(rdsl::created_at) .fetch_one(&data.pool)
.get_result::<i64>(&mut conn)
.await .await
{ {
let created_at: i64 = row;
let lifetime = current_time - created_at; let lifetime = current_time - created_at;
if lifetime > 2592000 { if lifetime > 2592000 {
if let Err(error) = delete(refresh_tokens::table) if let Err(error) = sqlx::query("DELETE FROM refresh_tokens WHERE token = $1")
.filter(rdsl::token.eq(&refresh_token)) .bind(&refresh_token)
.execute(&mut conn) .execute(&data.pool)
.await .await
{ {
error!("{}", error); error!("{}", error);
} }
let mut refresh_token_cookie = refresh_token_cookie(refresh_token);
refresh_token_cookie.make_removal(); refresh_token_cookie.make_removal();
return Ok(HttpResponse::Unauthorized() return Ok(HttpResponse::Unauthorized()
@ -52,19 +51,29 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
.finish()); .finish());
} }
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
if lifetime > 1987200 { if lifetime > 1987200 {
let new_refresh_token = generate_token::<32>()?; let new_refresh_token = generate_refresh_token();
match update(refresh_tokens::table) if new_refresh_token.is_err() {
.filter(rdsl::token.eq(&refresh_token)) error!("{}", new_refresh_token.unwrap_err());
.set(( return Ok(HttpResponse::InternalServerError().finish());
rdsl::token.eq(&new_refresh_token), }
rdsl::created_at.eq(current_time),
)) let new_refresh_token = new_refresh_token.unwrap();
.execute(&mut conn)
.await match sqlx::query(
"UPDATE refresh_tokens SET token = $1, created_at = $2 WHERE token = $3",
)
.bind(&new_refresh_token)
.bind(current_time)
.bind(&refresh_token)
.execute(&data.pool)
.await
{ {
Ok(_) => { Ok(_) => {
refresh_token = new_refresh_token; refresh_token = new_refresh_token;
@ -75,22 +84,35 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
} }
} }
let access_token = generate_token::<16>()?; let access_token = generate_access_token();
update(access_tokens::table) if access_token.is_err() {
.filter(dsl::refresh_token.eq(&refresh_token)) error!("{}", access_token.unwrap_err());
.set(( return Ok(HttpResponse::InternalServerError().finish());
dsl::token.eq(&access_token), }
dsl::created_at.eq(current_time),
)) let access_token = access_token.unwrap();
.execute(&mut conn)
.await?; if let Err(error) = sqlx::query(
"UPDATE access_tokens SET token = $1, created_at = $2 WHERE refresh_token = $3",
)
.bind(&access_token)
.bind(current_time)
.bind(&refresh_token)
.execute(&data.pool)
.await
{
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
return Ok(HttpResponse::Ok() return Ok(HttpResponse::Ok()
.cookie(new_refresh_token_cookie(&data.config, refresh_token)) .cookie(refresh_token_cookie(refresh_token))
.json(Response { access_token })); .json(Response { access_token }));
} }
let mut refresh_token_cookie = refresh_token_cookie(refresh_token);
refresh_token_cookie.make_removal(); refresh_token_cookie.make_removal();
Ok(HttpResponse::Unauthorized() Ok(HttpResponse::Unauthorized()

View file

@ -1,28 +1,19 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{HttpResponse, post, web}; use actix_web::{Error, HttpResponse, post, web};
use argon2::{ use argon2::{
PasswordHasher, PasswordHasher,
password_hash::{SaltString, rand_core::OsRng}, password_hash::{SaltString, rand_core::OsRng},
}; };
use diesel::{ExpressionMethods, dsl::insert_into}; use log::error;
use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use super::Response; use super::Response;
use crate::{ use crate::{
Data, Data,
error::Error, api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX},
schema::{ utils::{generate_access_token, generate_refresh_token, refresh_token_cookie},
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_token,
new_refresh_token_cookie,
},
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@ -69,12 +60,6 @@ pub async fn res(
account_information: web::Json<AccountInformation>, account_information: web::Json<AccountInformation>,
data: web::Data<Data>, data: web::Data<Data>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
if !data.config.instance.registration {
return Err(Error::Forbidden(
"registration is disabled on this instance".to_string(),
));
}
let uuid = Uuid::now_v7(); let uuid = Uuid::now_v7();
if !EMAIL_REGEX.is_match(&account_information.email) { if !EMAIL_REGEX.is_match(&account_information.email) {
@ -107,47 +92,91 @@ pub async fn res(
.argon2 .argon2
.hash_password(account_information.password.as_bytes(), &salt) .hash_password(account_information.password.as_bytes(), &salt)
{ {
let mut conn = data.pool.get().await?;
// TODO: Check security of this implementation // TODO: Check security of this implementation
insert_into(users::table) return Ok(
.values(( match sqlx::query(&format!(
udsl::uuid.eq(uuid), "INSERT INTO users (uuid, username, password, email) VALUES ( '{}', $1, $2, $3 )",
udsl::username.eq(&account_information.identifier), uuid
udsl::password.eq(hashed_password.to_string()),
udsl::email.eq(&account_information.email),
)) ))
.execute(&mut conn) .bind(&account_information.identifier)
.await?; .bind(hashed_password.to_string())
.bind(&account_information.email)
.execute(&data.pool)
.await
{
Ok(_out) => {
let refresh_token = generate_refresh_token();
let access_token = generate_access_token();
let refresh_token = generate_token::<32>()?; if refresh_token.is_err() {
let access_token = generate_token::<16>()?; error!("{}", refresh_token.unwrap_err());
return Ok(HttpResponse::InternalServerError().finish());
}
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let refresh_token = refresh_token.unwrap();
insert_into(refresh_tokens::table) if access_token.is_err() {
.values(( error!("{}", access_token.unwrap_err());
rdsl::token.eq(&refresh_token), return Ok(HttpResponse::InternalServerError().finish());
rdsl::uuid.eq(uuid), }
rdsl::created_at.eq(current_time),
rdsl::device_name.eq(&account_information.device_name),
))
.execute(&mut conn)
.await?;
insert_into(access_tokens::table) let access_token = access_token.unwrap();
.values((
adsl::token.eq(&access_token),
adsl::refresh_token.eq(&refresh_token),
adsl::uuid.eq(uuid),
adsl::created_at.eq(current_time),
))
.execute(&mut conn)
.await?;
return Ok(HttpResponse::Ok() let current_time = SystemTime::now()
.cookie(new_refresh_token_cookie(&data.config, refresh_token)) .duration_since(UNIX_EPOCH)
.json(Response { access_token })); .unwrap()
.as_secs() as i64;
if let Err(error) = sqlx::query(&format!("INSERT INTO refresh_tokens (token, uuid, created_at, device_name) VALUES ($1, '{}', $2, $3 )", uuid))
.bind(&refresh_token)
.bind(current_time)
.bind(&account_information.device_name)
.execute(&data.pool)
.await {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish())
}
if let Err(error) = sqlx::query(&format!("INSERT INTO access_tokens (token, refresh_token, uuid, created_at) VALUES ($1, $2, '{}', $3 )", uuid))
.bind(&access_token)
.bind(&refresh_token)
.bind(current_time)
.execute(&data.pool)
.await {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish())
}
HttpResponse::Ok()
.cookie(refresh_token_cookie(refresh_token))
.json(Response { access_token })
}
Err(error) => {
let err_msg = error.as_database_error().unwrap().message();
match err_msg {
err_msg
if err_msg.contains("unique") && err_msg.contains("username_key") =>
{
HttpResponse::Forbidden().json(ResponseError {
gorb_id_available: false,
..Default::default()
})
}
err_msg if err_msg.contains("unique") && err_msg.contains("email_key") => {
HttpResponse::Forbidden().json(ResponseError {
email_available: false,
..Default::default()
})
}
_ => {
error!("{}", err_msg);
HttpResponse::InternalServerError().finish()
}
}
}
},
);
} }
Ok(HttpResponse::InternalServerError().finish()) Ok(HttpResponse::InternalServerError().finish())

View file

@ -1,83 +0,0 @@
//! `/api/v1/auth/reset-password` Endpoints for resetting user password
use actix_web::{HttpResponse, get, post, web};
use chrono::{Duration, Utc};
use serde::Deserialize;
use crate::{Data, error::Error, objects::PasswordResetToken};
#[derive(Deserialize)]
struct Query {
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
///
#[get("/reset-password")]
pub async fn get(query: web::Query<Query>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
if let Ok(password_reset_token) =
PasswordResetToken::get_with_identifier(&data, query.identifier.clone()).await
{
if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) {
password_reset_token.delete(&data).await?;
} else {
return Err(Error::TooManyRequests(
"Please allow 1 hour before sending a new email".to_string(),
));
}
}
PasswordResetToken::new(&data, query.identifier.clone()).await?;
Ok(HttpResponse::Ok().finish())
}
#[derive(Deserialize)]
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
///
#[post("/reset-password")]
pub async fn post(
reset_password: web::Json<ResetPassword>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let password_reset_token =
PasswordResetToken::get(&data, reset_password.token.clone()).await?;
password_reset_token
.set_password(&data, reset_password.password.clone())
.await?;
Ok(HttpResponse::Ok().finish())
}

View file

@ -1,17 +1,10 @@
use actix_web::{HttpRequest, HttpResponse, post, web}; use actix_web::{Error, HttpRequest, HttpResponse, post, web};
use argon2::{PasswordHash, PasswordVerifier}; use argon2::{PasswordHash, PasswordVerifier};
use diesel::{ExpressionMethods, QueryDsl, delete}; use futures::future;
use diesel_async::RunQueryDsl; use log::error;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use crate::{ use crate::{Data, api::v1::auth::check_access_token, utils::get_auth_header};
Data,
api::v1::auth::check_access_token,
error::Error,
schema::refresh_tokens::{self, dsl as rdsl},
schema::users::dsl as udsl,
utils::get_auth_header,
};
#[derive(Deserialize)] #[derive(Deserialize)]
struct RevokeRequest { struct RevokeRequest {
@ -19,6 +12,17 @@ struct RevokeRequest {
device_name: String, device_name: String,
} }
#[derive(Serialize)]
struct Response {
deleted: bool,
}
impl Response {
fn new(deleted: bool) -> Self {
Self { deleted }
}
}
// TODO: Should maybe be a delete request? // TODO: Should maybe be a delete request?
#[post("/revoke")] #[post("/revoke")]
pub async fn res( pub async fn res(
@ -28,36 +32,85 @@ pub async fn res(
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let headers = req.headers(); let headers = req.headers();
let auth_header = get_auth_header(headers)?; let auth_header = get_auth_header(headers);
let mut conn = data.pool.get().await?; if let Err(error) = auth_header {
return Ok(error);
}
let uuid = check_access_token(auth_header, &mut conn).await?; let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
let database_password: String = udsl::users if let Err(error) = authorized {
.filter(udsl::uuid.eq(uuid)) return Ok(error);
.select(udsl::password) }
.get_result(&mut conn)
.await?;
let hashed_password = PasswordHash::new(&database_password) let uuid = authorized.unwrap();
.map_err(|e| Error::PasswordHashError(e.to_string()))?;
let database_password_raw = sqlx::query_scalar(&format!(
"SELECT password FROM users WHERE uuid = '{}'",
uuid
))
.fetch_one(&data.pool)
.await;
if let Err(error) = database_password_raw {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().json(Response::new(false)));
}
let database_password: String = database_password_raw.unwrap();
let hashed_password_raw = PasswordHash::new(&database_password);
if let Err(error) = hashed_password_raw {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().json(Response::new(false)));
}
let hashed_password = hashed_password_raw.unwrap();
if data if data
.argon2 .argon2
.verify_password(revoke_request.password.as_bytes(), &hashed_password) .verify_password(revoke_request.password.as_bytes(), &hashed_password)
.is_err() .is_err()
{ {
return Err(Error::Unauthorized( return Ok(HttpResponse::Unauthorized().finish());
"Wrong username or password".to_string(),
));
} }
delete(refresh_tokens::table) let tokens_raw = sqlx::query_scalar(&format!(
.filter(rdsl::uuid.eq(uuid)) "SELECT token FROM refresh_tokens WHERE uuid = '{}' AND device_name = $1",
.filter(rdsl::device_name.eq(&revoke_request.device_name)) uuid
.execute(&mut conn) ))
.await?; .bind(&revoke_request.device_name)
.fetch_all(&data.pool)
.await;
Ok(HttpResponse::Ok().finish()) if tokens_raw.is_err() {
error!("{:?}", tokens_raw);
return Ok(HttpResponse::InternalServerError().json(Response::new(false)));
}
let tokens: Vec<String> = tokens_raw.unwrap();
let mut refresh_tokens_delete = vec![];
for token in tokens {
refresh_tokens_delete.push(
sqlx::query("DELETE FROM refresh_tokens WHERE token = $1")
.bind(token.clone())
.execute(&data.pool),
);
}
let results = future::join_all(refresh_tokens_delete).await;
let errors: Vec<&Result<sqlx::postgres::PgQueryResult, sqlx::Error>> =
results.iter().filter(|r| r.is_err()).collect();
if !errors.is_empty() {
error!("{:?}", errors);
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(Response::new(true)))
} }

View file

@ -1,101 +0,0 @@
//! `/api/v1/auth/verify-email` Endpoints for verifying user emails
use actix_web::{HttpRequest, HttpResponse, get, post, web};
use chrono::{Duration, Utc};
use serde::Deserialize;
use crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::{EmailToken, Me},
utils::get_auth_header,
};
#[derive(Deserialize)]
struct Query {
token: String,
}
/// `GET /api/v1/auth/verify-email` Verifies user email address
///
/// requires auth? yes
///
/// ### Query Parameters
/// token
///
/// ### Responses
/// 200 Success
/// 410 Token Expired
/// 404 Not Found
/// 401 Unauthorized
///
#[get("/verify-email")]
pub async fn get(
req: HttpRequest,
query: web::Query<Query>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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?;
let email_token = EmailToken::get(&data, me.uuid).await?;
if query.token != email_token.token {
return Ok(HttpResponse::Unauthorized().finish());
}
me.verify_email(&mut conn).await?;
email_token.delete(&data).await?;
Ok(HttpResponse::Ok().finish())
}
/// `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
///
#[post("/verify-email")]
pub async fn post(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
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?;
if me.email_verified {
return Ok(HttpResponse::NoContent().finish());
}
if let Ok(email_token) = EmailToken::get(&data, me.uuid).await {
if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(1) {
email_token.delete(&data).await?;
} else {
return Err(Error::TooManyRequests(
"Please allow 1 hour before sending a new email".to_string(),
));
}
}
EmailToken::new(&data, me).await?;
Ok(HttpResponse::Ok().finish())
}

View file

@ -1,12 +0,0 @@
use actix_web::{Scope, web};
mod uuid;
pub fn web() -> Scope {
web::scope("/channels")
.service(uuid::get)
.service(uuid::delete)
.service(uuid::patch)
.service(uuid::messages::get)
.service(uuid::socket::ws)
}

View file

@ -1,78 +0,0 @@
//! `/api/v1/channels/{uuid}/messages` Endpoints related to channel messages
use crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::{Channel, Member},
utils::{get_auth_header, global_checks},
};
use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, web};
use serde::Deserialize;
#[derive(Deserialize)]
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"
/// }
/// });
/// ```
///
#[get("/{uuid}/messages")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
message_request: web::Query<MessageRequest>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let channel_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let channel = Channel::fetch_one(&data, channel_uuid).await?;
Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
let messages = channel
.fetch_messages(&data, message_request.amount, message_request.offset)
.await?;
Ok(HttpResponse::Ok().json(messages))
}

View file

@ -1,147 +0,0 @@
//! `/api/v1/channels/{uuid}` Channel specific endpoints
pub mod messages;
pub mod socket;
use crate::{
api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, utils::{get_auth_header, global_checks}, Data
};
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
use serde::Deserialize;
use uuid::Uuid;
#[get("/{uuid}")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let channel_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let channel = Channel::fetch_one(&data, channel_uuid).await?;
Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
Ok(HttpResponse::Ok().json(channel))
}
#[delete("/{uuid}")]
pub async fn delete(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let channel_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let channel = Channel::fetch_one(&data, channel_uuid).await?;
let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
member.check_permission(&data, Permissions::DeleteChannel).await?;
channel.delete(&data).await?;
Ok(HttpResponse::Ok().finish())
}
#[derive(Deserialize)]
struct NewInfo {
name: Option<String>,
description: Option<String>,
is_above: Option<String>,
}
/// `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
#[patch("/{uuid}")]
pub async fn patch(
req: HttpRequest,
path: web::Path<(Uuid,)>,
new_info: web::Json<NewInfo>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let channel_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let mut channel = Channel::fetch_one(&data, channel_uuid).await?;
let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
member.check_permission(&data, Permissions::ManageChannel).await?;
if let Some(new_name) = &new_info.name {
channel.set_name(&data, new_name.to_string()).await?;
}
if let Some(new_description) = &new_info.description {
channel
.set_description(&data, new_description.to_string())
.await?;
}
if let Some(new_is_above) = &new_info.is_above {
channel
.set_description(&data, new_is_above.to_string())
.await?;
}
Ok(HttpResponse::Ok().json(channel))
}

View file

@ -1,111 +0,0 @@
use actix_web::{
Error, HttpRequest, HttpResponse, get,
http::header::{HeaderValue, SEC_WEBSOCKET_PROTOCOL},
rt, web,
};
use actix_ws::AggregatedMessage;
use futures_util::StreamExt as _;
use uuid::Uuid;
use crate::{
Data,
api::v1::auth::check_access_token,
objects::{Channel, Member},
utils::{get_ws_protocol_header, global_checks},
};
#[get("/{uuid}/socket")]
pub async fn ws(
req: HttpRequest,
path: web::Path<(Uuid,)>,
stream: web::Payload,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
// Get all headers
let headers = req.headers();
// Retrieve auth header
let auth_header = get_ws_protocol_header(headers)?;
// Get uuid from path
let channel_uuid = path.into_inner().0;
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?;
global_checks(&data, uuid).await?;
let channel = Channel::fetch_one(&data, channel_uuid).await?;
Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
let (mut 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?;
while let Some(msg) = pubsub.on_message().next().await {
let payload: String = msg.get_payload()?;
session_1.text(payload).await?;
}
Ok::<(), crate::error::Error>(())
});
// start task but don't wait for it
rt::spawn(async move {
// receive messages from websocket
while let Some(msg) = stream.next().await {
match msg {
Ok(AggregatedMessage::Text(text)) => {
let mut conn = data.cache_pool.get_multiplexed_tokio_connection().await?;
let message = channel.new_message(&data, uuid, text.to_string()).await?;
redis::cmd("PUBLISH")
.arg(&[channel_uuid.to_string(), serde_json::to_string(&message)?])
.exec_async(&mut conn)
.await?;
}
Ok(AggregatedMessage::Binary(bin)) => {
// echo binary message
session_2.binary(bin).await?;
}
Ok(AggregatedMessage::Ping(msg)) => {
// respond to PING frame with PONG frame
session_2.pong(&msg).await?;
}
_ => {}
}
}
Ok::<(), crate::error::Error>(())
});
let headers = res.headers_mut();
headers.append(
SEC_WEBSOCKET_PROTOCOL,
HeaderValue::from_str("Authorization")?,
);
// respond immediately with response connected to WS session
Ok(res)
}

View file

@ -1,139 +0,0 @@
//! `/api/v1/guilds` Guild related endpoints
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,
objects::{Guild, StartAmountQuery},
utils::{get_auth_header, global_checks},
};
#[derive(Deserialize)]
struct GuildInfo {
name: String,
}
pub fn web() -> Scope {
web::scope("/guilds")
.service(post)
.service(get)
.service(uuid::web())
}
/// `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
#[post("")]
pub async fn post(
req: HttpRequest,
guild_info: web::Json<GuildInfo>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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(), uuid).await?;
Ok(HttpResponse::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
#[get("")]
pub async fn get(
req: HttpRequest,
request_query: web::Query<StartAmountQuery>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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);
let uuid = check_access_token(auth_header, &mut data.pool.get().await?).await?;
global_checks(&data, uuid).await?;
let guilds = Guild::fetch_amount(&data.pool, start, amount).await?;
Ok(HttpResponse::Ok().json(guilds))
}

View file

@ -1,86 +0,0 @@
use crate::{
api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, utils::{get_auth_header, global_checks, order_by_is_above}, Data
};
use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, post, web};
use serde::Deserialize;
#[derive(Deserialize)]
struct ChannelInfo {
name: String,
description: Option<String>,
}
#[get("{uuid}/channels")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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?;
global_checks(&data, uuid).await?;
Member::check_membership(&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?;
let channels_ordered = order_by_is_above(channels).await?;
data.set_cache_key(
format!("{}_channels", guild_uuid),
channels_ordered.clone(),
1800,
)
.await?;
Ok(HttpResponse::Ok().json(channels_ordered))
}
#[post("{uuid}/channels")]
pub async fn create(
req: HttpRequest,
channel_info: web::Json<ChannelInfo>,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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?;
global_checks(&data, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member.check_permission(&data, Permissions::CreateChannel).await?;
let channel = Channel::new(
data.clone(),
guild_uuid,
channel_info.name.clone(),
channel_info.description.clone(),
)
.await?;
Ok(HttpResponse::Ok().json(channel))
}

View file

@ -1,56 +0,0 @@
//! `/api/v1/guilds/{uuid}/icon` icon related endpoints, will probably be replaced by a multipart post to above endpoint
use actix_web::{HttpRequest, HttpResponse, put, web};
use futures_util::StreamExt as _;
use uuid::Uuid;
use crate::{
api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, utils::{get_auth_header, global_checks}, Data
};
/// `PUT /api/v1/guilds/{uuid}/icon` Icon upload
///
/// requires auth: no
///
/// put request expects a file and nothing else
#[put("{uuid}/icon")]
pub async fn upload(
req: HttpRequest,
path: web::Path<(Uuid,)>,
mut payload: web::Payload,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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?;
global_checks(&data, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member.check_permission(&data, Permissions::ManageServer).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.bunny_storage,
&mut conn,
data.config.bunny.cdn_url.clone(),
bytes,
)
.await?;
Ok(HttpResponse::Ok().finish())
}

View file

@ -1,69 +0,0 @@
use actix_web::{HttpRequest, HttpResponse, get, post, web};
use serde::Deserialize;
use uuid::Uuid;
use crate::{
api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, utils::{get_auth_header, global_checks}, Data
};
#[derive(Deserialize)]
struct InviteRequest {
custom_id: Option<String>,
}
#[get("{uuid}/invites")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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?;
global_checks(&data, 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(HttpResponse::Ok().json(invites))
}
#[post("{uuid}/invites")]
pub async fn create(
req: HttpRequest,
path: web::Path<(Uuid,)>,
invite_request: web::Json<InviteRequest>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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?;
global_checks(&data, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member.check_permission(&data, 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(HttpResponse::Ok().json(invite))
}

View file

@ -1,34 +0,0 @@
use crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::Member,
utils::{get_auth_header, global_checks},
};
use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, web};
#[get("{uuid}/members")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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?;
global_checks(&data, uuid).await?;
Member::check_membership(&mut conn, uuid, guild_uuid).await?;
let members = Member::fetch_all(&data, guild_uuid).await?;
Ok(HttpResponse::Ok().json(members))
}

View file

@ -1,96 +0,0 @@
//! `/api/v1/guilds/{uuid}` Specific server endpoints
use actix_web::{HttpRequest, HttpResponse, Scope, get, web};
use uuid::Uuid;
mod channels;
mod icon;
mod invites;
mod members;
mod roles;
use crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::{Guild, Member},
utils::{get_auth_header, global_checks},
};
pub fn web() -> Scope {
web::scope("")
// Servers
.service(get)
// Channels
.service(channels::get)
.service(channels::create)
// Roles
.service(roles::get)
.service(roles::create)
.service(roles::uuid::get)
// Invites
.service(invites::get)
.service(invites::create)
// Icon
.service(icon::upload)
// Members
.service(members::get)
}
/// `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
/// });
/// ```
#[get("/{uuid}")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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?;
global_checks(&data, uuid).await?;
Member::check_membership(&mut conn, uuid, guild_uuid).await?;
let guild = Guild::fetch_one(&mut conn, guild_uuid).await?;
Ok(HttpResponse::Ok().json(guild))
}

View file

@ -1,76 +0,0 @@
use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, post, web};
use serde::Deserialize;
use crate::{
api::v1::auth::check_access_token, error::Error, objects::{Member, Permissions, Role}, utils::{get_auth_header, global_checks, order_by_is_above}, Data
};
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<Data>,
) -> Result<HttpResponse, Error> {
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::check_membership(&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?;
let roles_ordered = order_by_is_above(roles).await?;
data.set_cache_key(format!("{}_roles", guild_uuid), roles_ordered.clone(), 1800)
.await?;
Ok(HttpResponse::Ok().json(roles_ordered))
}
#[post("{uuid}/roles")]
pub async fn create(
req: HttpRequest,
role_info: web::Json<RoleInfo>,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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?;
global_checks(&data, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member.check_permission(&data, Permissions::CreateRole).await?;
let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?;
Ok(HttpResponse::Ok().json(role))
}

View file

@ -1,43 +0,0 @@
use crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::{Member, Role},
utils::{get_auth_header, global_checks},
};
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<Data>,
) -> Result<HttpResponse, Error> {
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?;
global_checks(&data, uuid).await?;
Member::check_membership(&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))
}

View file

@ -1,22 +1,43 @@
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use actix_web::{Error, HttpRequest, HttpResponse, get, post, web};
use crate::{ use crate::{
Data, Data,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, structs::{Guild, Invite, Member},
objects::{Guild, Invite, Member}, utils::get_auth_header,
utils::{get_auth_header, global_checks},
}; };
#[get("{id}")] #[get("{id}")]
pub async fn get(path: web::Path<(String,)>, data: web::Data<Data>) -> Result<HttpResponse, Error> { pub async fn get(
let mut conn = data.pool.get().await?; req: HttpRequest,
path: web::Path<(String,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let invite_id = path.into_inner().0; let invite_id = path.into_inner().0;
let invite = Invite::fetch_one(&mut conn, invite_id).await?; let result = Invite::fetch_one(&data.pool, invite_id).await;
let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; if let Err(error) = result {
return Ok(error);
}
let invite = result.unwrap();
let guild_result = Guild::fetch_one(&data.pool, invite.guild_uuid).await;
if let Err(error) = guild_result {
return Ok(error);
}
let guild = guild_result.unwrap();
Ok(HttpResponse::Ok().json(guild)) Ok(HttpResponse::Ok().json(guild))
} }
@ -29,21 +50,43 @@ pub async fn join(
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let headers = req.headers(); let headers = req.headers();
let auth_header = get_auth_header(headers)?; let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let invite_id = path.into_inner().0; let invite_id = path.into_inner().0;
let mut conn = data.pool.get().await?; let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
let uuid = check_access_token(auth_header, &mut conn).await?; if let Err(error) = authorized {
return Ok(error);
}
global_checks(&data, uuid).await?; let uuid = authorized.unwrap();
let invite = Invite::fetch_one(&mut conn, invite_id).await?; let result = Invite::fetch_one(&data.pool, invite_id).await;
let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; if let Err(error) = result {
return Ok(error);
}
Member::new(&data, uuid, guild.uuid).await?; let invite = result.unwrap();
let guild_result = Guild::fetch_one(&data.pool, invite.guild_uuid).await;
if let Err(error) = guild_result {
return Ok(error);
}
let guild = guild_result.unwrap();
let member = Member::new(&data.pool, uuid, guild.uuid).await;
if let Err(error) = member {
return Ok(error);
}
Ok(HttpResponse::Ok().json(guild)) Ok(HttpResponse::Ok().json(guild))
} }

View file

@ -1,75 +0,0 @@
//! `/api/v1/me/guilds` Contains endpoint related to guild memberships
use actix_web::{HttpRequest, HttpResponse, get, web};
use crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, 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
#[get("/guilds")]
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
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?;
global_checks(&data, uuid).await?;
let me = Me::get(&mut conn, uuid).await?;
let memberships = me.fetch_memberships(&mut conn).await?;
Ok(HttpResponse::Ok().json(memberships))
}

View file

@ -1,104 +0,0 @@
use actix_multipart::form::{MultipartForm, json::Json as MpJson, tempfile::TempFile};
use actix_web::{HttpRequest, HttpResponse, Scope, get, patch, web};
use serde::Deserialize;
use crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, global_checks},
};
mod guilds;
pub fn web() -> Scope {
web::scope("/me")
.service(get)
.service(update)
.service(guilds::get)
}
#[get("")]
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
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, Clone)]
struct NewInfo {
username: Option<String>,
display_name: Option<String>,
//password: Option<String>, will probably be handled through a reset password link
email: Option<String>,
pronouns: Option<String>,
about: Option<String>,
}
#[derive(Debug, MultipartForm)]
struct UploadForm {
#[multipart(limit = "100MB")]
avatar: Option<TempFile>,
json: MpJson<NewInfo>,
}
#[patch("")]
pub async fn update(
req: HttpRequest,
MultipartForm(form): MultipartForm<UploadForm>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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?;
if form.avatar.is_some() || form.json.username.is_some() || form.json.display_name.is_some() {
global_checks(&data, uuid).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, data.config.bunny.cdn_url.clone(), byte_slice.into())
.await?;
}
if let Some(username) = &form.json.username {
me.set_username(&data, username.clone()).await?;
}
if let Some(display_name) = &form.json.display_name {
me.set_display_name(&data, display_name.clone()).await?;
}
if let Some(email) = &form.json.email {
me.set_email(&data, email.clone()).await?;
}
if let Some(pronouns) = &form.json.pronouns {
me.set_pronouns(&data, pronouns.clone()).await?;
}
if let Some(about) = &form.json.about {
me.set_about(&data, about.clone()).await?;
}
Ok(HttpResponse::Ok().finish())
}

View file

@ -1,12 +1,8 @@
//! `/api/v1` Contains version 1 of the api
use actix_web::{Scope, web}; use actix_web::{Scope, web};
mod auth; mod auth;
mod channels;
mod guilds;
mod invites; mod invites;
mod me; mod servers;
mod stats; mod stats;
mod users; mod users;
@ -15,8 +11,6 @@ pub fn web() -> Scope {
.service(stats::res) .service(stats::res)
.service(auth::web()) .service(auth::web())
.service(users::web()) .service(users::web())
.service(channels::web()) .service(servers::web())
.service(guilds::web())
.service(invites::web()) .service(invites::web())
.service(me::web())
} }

90
src/api/v1/servers/mod.rs Normal file
View file

@ -0,0 +1,90 @@
use actix_web::{get, post, web, Error, HttpRequest, HttpResponse, Scope};
use serde::Deserialize;
mod uuid;
use crate::{api::v1::auth::check_access_token, structs::{Guild, StartAmountQuery}, utils::get_auth_header, Data};
#[derive(Deserialize)]
struct GuildInfo {
name: String,
description: Option<String>,
}
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<GuildInfo>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let guild = Guild::new(
&data.pool,
guild_info.name.clone(),
guild_info.description.clone(),
uuid,
)
.await;
if let Err(error) = guild {
return Ok(error);
}
Ok(HttpResponse::Ok().json(guild.unwrap()))
}
#[get("")]
pub async fn get(
req: HttpRequest,
request_query: web::Query<StartAmountQuery>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
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 let Err(error) = auth_header {
return Ok(error);
}
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let guilds = Guild::fetch_amount(&data.pool, start, amount).await;
if let Err(error) = guilds {
return Ok(error);
}
Ok(HttpResponse::Ok().json(guilds.unwrap()))
}

View file

@ -0,0 +1,124 @@
use crate::{
Data,
api::v1::auth::check_access_token,
structs::{Channel, Member},
utils::get_auth_header,
};
use ::uuid::Uuid;
use actix_web::{Error, HttpRequest, HttpResponse, get, post, web};
use log::error;
use serde::Deserialize;
pub mod uuid;
#[derive(Deserialize)]
struct ChannelInfo {
name: String,
description: Option<String>,
}
#[get("{uuid}/channels")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let guild_uuid = path.into_inner().0;
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member {
return Ok(error);
}
let cache_result = data.get_cache_key(format!("{}_channels", guild_uuid)).await;
if let Ok(cache_hit) = cache_result {
return Ok(HttpResponse::Ok()
.content_type("application/json")
.body(cache_hit));
}
let channels_result = Channel::fetch_all(&data.pool, guild_uuid).await;
if let Err(error) = channels_result {
return Ok(error);
}
let channels = channels_result.unwrap();
let cache_result = data
.set_cache_key(format!("{}_channels", guild_uuid), channels.clone(), 1800)
.await;
if let Err(error) = cache_result {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(channels))
}
#[post("{uuid}/channels")]
pub async fn create(
req: HttpRequest,
channel_info: web::Json<ChannelInfo>,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let guild_uuid = path.into_inner().0;
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member {
return Ok(error);
}
// 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;
if let Err(error) = channel {
return Ok(error);
}
Ok(HttpResponse::Ok().json(channel.unwrap()))
}

View file

@ -0,0 +1,83 @@
use crate::{
Data,
api::v1::auth::check_access_token,
structs::{Channel, Member},
utils::get_auth_header,
};
use ::uuid::Uuid;
use actix_web::{Error, HttpRequest, HttpResponse, get, web};
use log::error;
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<MessageRequest>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let (guild_uuid, channel_uuid) = path.into_inner();
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member {
return Ok(error);
}
let cache_result = data.get_cache_key(format!("{}", channel_uuid)).await;
let channel: Channel;
if let Ok(cache_hit) = cache_result {
channel = serde_json::from_str(&cache_hit).unwrap()
} else {
let channel_result = Channel::fetch_one(&data.pool, guild_uuid, channel_uuid).await;
if let Err(error) = channel_result {
return Ok(error);
}
channel = channel_result.unwrap();
let cache_result = data
.set_cache_key(format!("{}", channel_uuid), channel.clone(), 60)
.await;
if let Err(error) = cache_result {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
}
let messages = channel
.fetch_messages(&data.pool, message_request.amount, message_request.offset)
.await;
if let Err(error) = messages {
return Ok(error);
}
Ok(HttpResponse::Ok().json(messages.unwrap()))
}

View file

@ -0,0 +1,131 @@
pub mod messages;
pub mod socket;
use crate::{
Data,
api::v1::auth::check_access_token,
structs::{Channel, Member},
utils::get_auth_header,
};
use ::uuid::Uuid;
use actix_web::{Error, HttpRequest, HttpResponse, delete, get, web};
use log::error;
#[get("{uuid}/channels/{channel_uuid}")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid, Uuid)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let (guild_uuid, channel_uuid) = path.into_inner();
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member {
return Ok(error);
}
let cache_result = data.get_cache_key(format!("{}", channel_uuid)).await;
if let Ok(cache_hit) = cache_result {
return Ok(HttpResponse::Ok()
.content_type("application/json")
.body(cache_hit));
}
let channel_result = Channel::fetch_one(&data.pool, guild_uuid, channel_uuid).await;
if let Err(error) = channel_result {
return Ok(error);
}
let channel = channel_result.unwrap();
let cache_result = data
.set_cache_key(format!("{}", channel_uuid), channel.clone(), 60)
.await;
if let Err(error) = cache_result {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(channel))
}
#[delete("{uuid}/channels/{channel_uuid}")]
pub async fn delete(
req: HttpRequest,
path: web::Path<(Uuid, Uuid)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let (guild_uuid, channel_uuid) = path.into_inner();
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member {
return Ok(error);
}
let cache_result = data.get_cache_key(format!("{}", channel_uuid)).await;
let channel: Channel;
if let Ok(cache_hit) = cache_result {
channel = serde_json::from_str(&cache_hit).unwrap();
let result = data.del_cache_key(format!("{}", channel_uuid)).await;
if let Err(error) = result {
error!("{}", error)
}
} else {
let channel_result = Channel::fetch_one(&data.pool, guild_uuid, channel_uuid).await;
if let Err(error) = channel_result {
return Ok(error);
}
channel = channel_result.unwrap();
}
let delete_result = channel.delete(&data.pool).await;
if let Err(error) = delete_result {
return Ok(error);
}
Ok(HttpResponse::Ok().finish())
}

View file

@ -0,0 +1,143 @@
use actix_web::{Error, HttpRequest, HttpResponse, get, rt, web};
use actix_ws::AggregatedMessage;
use futures_util::StreamExt as _;
use log::error;
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<Data>,
) -> Result<HttpResponse, Error> {
// Get all headers
let headers = req.headers();
// Retrieve auth header
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
// Get uuids from path
let (guild_uuid, channel_uuid) = path.into_inner();
// Authorize client using auth header
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
// Unwrap user uuid from authorization
let uuid = authorized.unwrap();
// Get server member from psql
let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member {
return Ok(error);
}
// Get cache for channel
let cache_result = data.get_cache_key(format!("{}", channel_uuid)).await;
let channel: Channel;
// Return channel cache or result from psql as `channel` variable
if let Ok(cache_hit) = cache_result {
channel = serde_json::from_str(&cache_hit).unwrap()
} else {
let channel_result = Channel::fetch_one(&data.pool, guild_uuid, channel_uuid).await;
if let Err(error) = channel_result {
return Ok(error);
}
channel = channel_result.unwrap();
let cache_result = data
.set_cache_key(format!("{}", channel_uuid), channel.clone(), 60)
.await;
if let Err(error) = cache_result {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
}
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 pubsub_result = data.cache_pool.get_async_pubsub().await;
if let Err(error) = pubsub_result {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
let mut session_2 = session_1.clone();
rt::spawn(async move {
let mut pubsub = pubsub_result.unwrap();
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(&data.pool, 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)
}

View file

@ -0,0 +1,43 @@
use actix_web::{delete, web, Error, HttpRequest, HttpResponse};
use uuid::Uuid;
use crate::{api::v1::auth::check_access_token, structs::{Invite, Member}, utils::get_auth_header, Data};
#[delete("{uuid}/invites/{id}")]
pub async fn delete(req: HttpRequest, path: web::Path<(Uuid, String)>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error)
}
let (guild_uuid, invite_id) = path.into_inner();
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error)
}
let uuid = authorized.unwrap();
if let Err(error) = Member::fetch_one(&data.pool, uuid, guild_uuid).await {
return Ok(error)
}
let result = Invite::fetch_one(&data.pool, invite_id).await;
if let Err(error) = result {
return Ok(error)
}
let invite = result.unwrap();
if let Err(error) = invite.delete(&data.pool).await {
return Ok(error)
}
Ok(HttpResponse::Ok().finish())
}

View file

@ -0,0 +1,116 @@
use actix_web::{Error, HttpRequest, HttpResponse, get, post, web};
use serde::Deserialize;
use uuid::Uuid;
mod id;
use crate::{
Data,
api::v1::auth::check_access_token,
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<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let guild_uuid = path.into_inner().0;
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member {
return Ok(error);
}
let guild_result = Guild::fetch_one(&data.pool, guild_uuid).await;
if let Err(error) = guild_result {
return Ok(error);
}
let guild = guild_result.unwrap();
let invites = guild.get_invites(&data.pool).await;
if let Err(error) = invites {
return Ok(error);
}
Ok(HttpResponse::Ok().json(invites.unwrap()))
}
#[post("{uuid}/invites")]
pub async fn create(
req: HttpRequest,
path: web::Path<(Uuid,)>,
invite_request: web::Json<Option<InviteRequest>>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let guild_uuid = path.into_inner().0;
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let member_result = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member_result {
return Ok(error);
}
let member = member_result.unwrap();
let guild_result = Guild::fetch_one(&data.pool, guild_uuid).await;
if let Err(error) = guild_result {
return Ok(error);
}
let guild = guild_result.unwrap();
let custom_id = invite_request.as_ref().map(|ir| ir.custom_id.clone());
let invite = guild.create_invite(&data.pool, &member, custom_id).await;
if let Err(error) = invite {
return Ok(error);
}
Ok(HttpResponse::Ok().json(invite.unwrap()))
}

View file

@ -0,0 +1,72 @@
use actix_web::{Error, HttpRequest, HttpResponse, Scope, get, web};
use uuid::Uuid;
mod channels;
mod invites;
mod roles;
use crate::{
Data,
api::v1::auth::check_access_token,
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)
}
#[get("/{uuid}")]
pub async fn res(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let guild_uuid = path.into_inner().0;
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member {
return Ok(error);
}
let guild = Guild::fetch_one(&data.pool, guild_uuid).await;
if let Err(error) = guild {
return Ok(error);
}
Ok(HttpResponse::Ok().json(guild.unwrap()))
}

View file

@ -0,0 +1,117 @@
use crate::{
Data,
api::v1::auth::check_access_token,
structs::{Member, Role},
utils::get_auth_header,
};
use ::uuid::Uuid;
use actix_web::{Error, HttpRequest, HttpResponse, get, post, web};
use log::error;
use serde::Deserialize;
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<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let guild_uuid = path.into_inner().0;
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member {
return Ok(error);
}
let cache_result = data.get_cache_key(format!("{}_roles", guild_uuid)).await;
if let Ok(cache_hit) = cache_result {
return Ok(HttpResponse::Ok()
.content_type("application/json")
.body(cache_hit));
}
let roles_result = Role::fetch_all(&data.pool, guild_uuid).await;
if let Err(error) = roles_result {
return Ok(error);
}
let roles = roles_result.unwrap();
let cache_result = data
.set_cache_key(format!("{}_roles", guild_uuid), roles.clone(), 1800)
.await;
if let Err(error) = cache_result {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(roles))
}
#[post("{uuid}/roles")]
pub async fn create(
req: HttpRequest,
role_info: web::Json<RoleInfo>,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let guild_uuid = path.into_inner().0;
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member {
return Ok(error);
}
// FIXME: Logic to check permissions, should probably be done in utils.rs
let role = Role::new(&data.pool, guild_uuid, role_info.name.clone()).await;
if let Err(error) = role {
return Ok(error);
}
Ok(HttpResponse::Ok().json(role.unwrap()))
}

View file

@ -0,0 +1,67 @@
use crate::{
Data,
api::v1::auth::check_access_token,
structs::{Member, Role},
utils::get_auth_header,
};
use ::uuid::Uuid;
use actix_web::{Error, HttpRequest, HttpResponse, get, web};
use log::error;
#[get("{uuid}/roles/{role_uuid}")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid, Uuid)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let (guild_uuid, role_uuid) = path.into_inner();
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await;
if let Err(error) = member {
return Ok(error);
}
let cache_result = data.get_cache_key(format!("{}", role_uuid)).await;
if let Ok(cache_hit) = cache_result {
return Ok(HttpResponse::Ok()
.content_type("application/json")
.body(cache_hit));
}
let role_result = Role::fetch_one(&data.pool, guild_uuid, role_uuid).await;
if let Err(error) = role_result {
return Ok(error);
}
let role = role_result.unwrap();
let cache_result = data
.set_cache_key(format!("{}", role_uuid), role.clone(), 60)
.await;
if let Err(error) = cache_result {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(role))
}

View file

@ -1,51 +1,31 @@
//! `/api/v1/stats` Returns stats about the server
use std::time::SystemTime; use std::time::SystemTime;
use actix_web::{HttpResponse, get, web}; use actix_web::{HttpResponse, Responder, get, web};
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use serde::Serialize; use serde::Serialize;
use crate::Data; 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 VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
const GIT_SHORT_HASH: &str = env!("GIT_SHORT_HASH");
#[derive(Serialize)] #[derive(Serialize)]
struct Response { struct Response {
accounts: i64, accounts: usize,
uptime: u64, uptime: u64,
version: String, version: String,
registration_enabled: bool,
email_verification_required: bool,
build_number: String, 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"
/// });
/// ```
#[get("/stats")] #[get("/stats")]
pub async fn res(data: web::Data<Data>) -> Result<HttpResponse, Error> { pub async fn res(data: web::Data<Data>) -> impl Responder {
let accounts: i64 = users let accounts;
.select(uuid) if let Ok(users) = sqlx::query("SELECT uuid FROM users")
.count() .fetch_all(&data.pool)
.get_result(&mut data.pool.get().await?) .await
.await?; {
accounts = users.len();
} else {
return HttpResponse::InternalServerError().finish();
}
let response = Response { let response = Response {
// TODO: Get number of accounts from db // TODO: Get number of accounts from db
@ -55,11 +35,9 @@ pub async fn res(data: web::Data<Data>) -> Result<HttpResponse, Error> {
.expect("Seriously why dont you have time??") .expect("Seriously why dont you have time??")
.as_secs(), .as_secs(),
version: String::from(VERSION.unwrap_or("UNKNOWN")), version: String::from(VERSION.unwrap_or("UNKNOWN")),
registration_enabled: data.config.instance.registration,
email_verification_required: data.config.instance.require_email_verification,
// TODO: Get build number from git hash or remove this from the spec // 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(HttpResponse::Ok().json(response)) HttpResponse::Ok().json(response)
} }

51
src/api/v1/users/me.rs Normal file
View file

@ -0,0 +1,51 @@
use actix_web::{Error, HttpRequest, HttpResponse, get, web};
use log::error;
use serde::Serialize;
use crate::{Data, api::v1::auth::check_access_token, utils::get_auth_header};
#[derive(Serialize)]
struct Response {
uuid: String,
username: String,
display_name: String,
}
#[get("/me")]
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let row = sqlx::query_as(&format!(
"SELECT username, display_name FROM users WHERE uuid = '{}'",
uuid
))
.fetch_one(&data.pool)
.await;
if let Err(error) = row {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
let (username, display_name): (String, Option<String>) = row.unwrap();
Ok(HttpResponse::Ok().json(Response {
uuid: uuid.to_string(),
username,
display_name: display_name.unwrap_or_default(),
}))
}

View file

@ -1,60 +1,36 @@
//! `/api/v1/users` Contains endpoints related to all users use crate::{api::v1::auth::check_access_token, structs::StartAmountQuery, utils::get_auth_header, Data};
use actix_web::{Error, HttpRequest, HttpResponse, Scope, get, web};
use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use log::error;
use serde::Serialize;
use crate::{ use sqlx::prelude::FromRow;
Data,
api::v1::auth::check_access_token,
error::Error,
objects::{StartAmountQuery, User},
utils::{get_auth_header, global_checks},
};
mod me;
mod uuid; mod uuid;
pub fn web() -> Scope { #[derive(Serialize, FromRow)]
web::scope("/users").service(get).service(uuid::get) struct Response {
uuid: String,
username: String,
display_name: Option<String>,
email: String,
}
pub fn web() -> Scope {
web::scope("/users")
.service(res)
.service(me::res)
.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
#[get("")] #[get("")]
pub async fn get( pub async fn res(
req: HttpRequest, req: HttpRequest,
request_query: web::Query<StartAmountQuery>, request_query: web::Query<StartAmountQuery>,
data: web::Data<Data>, data: web::Data<Data>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let headers = req.headers(); let headers = req.headers();
let auth_header = get_auth_header(headers)?; let auth_header = get_auth_header(headers);
let start = request_query.start.unwrap_or(0); let start = request_query.start.unwrap_or(0);
@ -64,13 +40,24 @@ pub async fn get(
return Ok(HttpResponse::BadRequest().finish()); return Ok(HttpResponse::BadRequest().finish());
} }
let mut conn = data.pool.get().await?; let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
let uuid = check_access_token(auth_header, &mut conn).await?; if let Err(error) = authorized {
return Ok(error);
}
global_checks(&data, uuid).await?; let row = sqlx::query_as("SELECT CAST(uuid AS VARCHAR), username, display_name, email FROM users ORDER BY username LIMIT $1 OFFSET $2")
.bind(amount)
.bind(start)
.fetch_all(&data.pool)
.await;
let users = User::fetch_amount(&mut conn, start, amount).await?; if let Err(error) = row {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(users)) let accounts: Vec<Response> = row.unwrap();
Ok(HttpResponse::Ok().json(accounts))
} }

View file

@ -1,51 +1,75 @@
//! `/api/v1/users/{uuid}` Specific user endpoints use actix_web::{Error, HttpRequest, HttpResponse, get, web};
use log::error;
use actix_web::{HttpRequest, HttpResponse, get, web}; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{Data, api::v1::auth::check_access_token, utils::get_auth_header};
Data,
api::v1::auth::check_access_token, #[derive(Serialize, Clone)]
error::Error, struct Response {
objects::User, uuid: String,
utils::{get_auth_header, global_checks}, username: String,
}; display_name: String,
}
/// `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
#[get("/{uuid}")] #[get("/{uuid}")]
pub async fn get( pub async fn res(
req: HttpRequest, req: HttpRequest,
path: web::Path<(Uuid,)>, path: web::Path<(Uuid,)>,
data: web::Data<Data>, data: web::Data<Data>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let headers = req.headers(); let headers = req.headers();
let user_uuid = path.into_inner().0; let uuid = path.into_inner().0;
let auth_header = get_auth_header(headers)?; let auth_header = get_auth_header(headers);
let mut conn = data.pool.get().await?; if let Err(error) = auth_header {
return Ok(error);
}
let uuid = check_access_token(auth_header, &mut conn).await?; let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
global_checks(&data, uuid).await?; if let Err(error) = authorized {
return Ok(error);
}
let user = User::fetch_one(&data, user_uuid).await?; let cache_result = data.get_cache_key(uuid.to_string()).await;
if let Ok(cache_hit) = cache_result {
return Ok(HttpResponse::Ok()
.content_type("application/json")
.body(cache_hit));
}
let row = sqlx::query_as(&format!(
"SELECT username, display_name FROM users WHERE uuid = '{}'",
uuid
))
.fetch_one(&data.pool)
.await;
if let Err(error) = row {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
let (username, display_name): (String, Option<String>) = row.unwrap();
let user = Response {
uuid: uuid.to_string(),
username,
display_name: display_name.unwrap_or_default(),
};
let cache_result = data
.set_cache_key(uuid.to_string(), user.clone(), 1800)
.await;
if let Err(error) = cache_result {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(user)) Ok(HttpResponse::Ok().json(user))
} }

View file

@ -1,4 +1,3 @@
//! `/api/v1/versions` Returns info about api versions
use actix_web::{HttpResponse, Responder, get}; use actix_web::{HttpResponse, Responder, get};
use serde::Serialize; use serde::Serialize;
@ -11,21 +10,8 @@ struct Response {
#[derive(Serialize)] #[derive(Serialize)]
struct UnstableFeatures; struct UnstableFeatures;
/// `GET /api/versions` Returns info about api versions.
///
/// requires auth: no
///
/// ### Response Example
/// ```
/// json!({
/// "unstable_features": {},
/// "versions": [
/// "1"
/// ]
/// });
/// ```
#[get("/versions")] #[get("/versions")]
pub async fn get() -> impl Responder { pub async fn res() -> impl Responder {
let response = Response { let response = Response {
unstable_features: UnstableFeatures, unstable_features: UnstableFeatures,
// TODO: Find a way to dynamically update this possibly? // TODO: Find a way to dynamically update this possibly?

View file

@ -1,19 +1,14 @@
use crate::error::Error; use crate::Error;
use bunny_api_tokio::edge_storage::Endpoint;
use lettre::transport::smtp::authentication::Credentials;
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use sqlx::postgres::PgConnectOptions;
use tokio::fs::read_to_string; use tokio::fs::read_to_string;
use url::Url;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ConfigBuilder { pub struct ConfigBuilder {
database: Database, database: Database,
cache_database: CacheDatabase, cache_database: CacheDatabase,
web: WebBuilder, web: Option<WebBuilder>,
instance: Option<InstanceBuilder>,
bunny: BunnyBuilder,
mail: Mail,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
@ -36,42 +31,11 @@ pub struct CacheDatabase {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct WebBuilder { struct WebBuilder {
ip: Option<String>, url: Option<String>,
port: Option<u16>, port: Option<u16>,
frontend_url: Url,
backend_url: Option<Url>,
_ssl: Option<bool>, _ssl: Option<bool>,
} }
#[derive(Debug, Deserialize)]
struct InstanceBuilder {
name: Option<String>,
registration: Option<bool>,
require_email_verification: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct BunnyBuilder {
api_key: String,
endpoint: String,
storage_zone: String,
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 { impl ConfigBuilder {
pub async fn load(path: String) -> Result<Self, Error> { pub async fn load(path: String) -> Result<Self, Error> {
debug!("loading config from: {}", path); debug!("loading config from: {}", path);
@ -83,57 +47,22 @@ impl ConfigBuilder {
} }
pub fn build(self) -> Config { pub fn build(self) -> Config {
let web = Web { let web = if let Some(web) = self.web {
ip: self.web.ip.unwrap_or(String::from("0.0.0.0")), Web {
port: self.web.port.unwrap_or(8080), url: web.url.unwrap_or(String::from("0.0.0.0")),
frontend_url: self.web.frontend_url.clone(), port: web.port.unwrap_or(8080),
backend_url: self }
.web } else {
.backend_url Web {
.or_else(|| self.web.frontend_url.join("/api").ok()) url: String::from("0.0.0.0"),
.unwrap(), port: 8080,
}; }
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 = Bunny {
api_key: self.bunny.api_key,
endpoint,
storage_zone: self.bunny.storage_zone,
cdn_url: self.bunny.cdn_url,
};
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),
},
None => Instance {
name: "Gorb".to_string(),
registration: true,
require_email_verification: false,
},
}; };
Config { Config {
database: self.database, database: self.database,
cache_database: self.cache_database, cache_database: self.cache_database,
web, web,
instance,
bunny,
mail: self.mail,
} }
} }
} }
@ -143,53 +72,22 @@ pub struct Config {
pub database: Database, pub database: Database,
pub cache_database: CacheDatabase, pub cache_database: CacheDatabase,
pub web: Web, pub web: Web,
pub instance: Instance,
pub bunny: Bunny,
pub mail: Mail,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Web { pub struct Web {
pub ip: String, pub url: String,
pub port: u16, 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,
}
#[derive(Debug, Clone)]
pub struct Bunny {
pub api_key: String,
pub endpoint: Endpoint,
pub storage_zone: String,
pub cdn_url: Url,
} }
impl Database { impl Database {
pub fn url(&self) -> String { pub fn connect_options(&self) -> PgConnectOptions {
let mut url = String::from("postgres://"); PgConnectOptions::new()
.database(&self.database)
url += &self.username; .host(&self.host)
.username(&self.username)
url += ":"; .password(&self.password)
url += &self.password; .port(self.port)
url += "@";
url += &self.host;
url += ":";
url += &self.port.to_string();
url += "/";
url += &self.database;
url
} }
} }
@ -222,9 +120,3 @@ impl CacheDatabase {
url url
} }
} }
impl Smtp {
pub fn credentials(&self) -> Credentials {
Credentials::new(self.username.clone(), self.password.clone())
}
}

View file

@ -1,112 +0,0 @@
use std::{io, time::SystemTimeError};
use actix_web::{
HttpResponse,
error::{PayloadError, ResponseError},
http::{
StatusCode,
header::{ContentType, ToStrError},
},
};
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;
use serde_json::Error as JsonError;
use thiserror::Error;
use tokio::task::JoinError;
use toml::de::Error as TomlError;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
SqlError(#[from] DieselError),
#[error(transparent)]
PoolError(#[from] PoolError<DieselPoolError>),
#[error(transparent)]
BuildError(#[from] BuildError),
#[error(transparent)]
RedisError(#[from] RedisError),
#[error(transparent)]
ConnectionError(#[from] ConnectionError),
#[error(transparent)]
JoinError(#[from] JoinError),
#[error(transparent)]
IoError(#[from] io::Error),
#[error(transparent)]
TomlError(#[from] TomlError),
#[error(transparent)]
JsonError(#[from] JsonError),
#[error(transparent)]
SystemTimeError(#[from] SystemTimeError),
#[error(transparent)]
ToStrError(#[from] ToStrError),
#[error(transparent)]
RandomError(#[from] getrandom::Error),
#[error(transparent)]
BunnyError(#[from] BunnyError),
#[error(transparent)]
UrlParseError(#[from] url::ParseError),
#[error(transparent)]
PayloadError(#[from] PayloadError),
#[error(transparent)]
WsClosed(#[from] actix_ws::Closed),
#[error(transparent)]
EmailError(#[from] EmailError),
#[error(transparent)]
SmtpError(#[from] SmtpError),
#[error(transparent)]
SmtpAddressError(#[from] AddressError),
#[error("{0}")]
PasswordHashError(String),
#[error("{0}")]
BadRequest(String),
#[error("{0}")]
Unauthorized(String),
#[error("{0}")]
Forbidden(String),
#[error("{0}")]
TooManyRequests(String),
#[error("{0}")]
InternalServerError(String),
}
impl ResponseError for Error {
fn error_response(&self) -> HttpResponse {
debug!("{:?}", self);
error!("{}: {}", self.status_code(), self);
HttpResponse::build(self.status_code())
.insert_header(ContentType::json())
.json(WebError::new(self.to_string()))
}
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,
Error::Forbidden(_) => StatusCode::FORBIDDEN,
Error::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
#[derive(Serialize)]
struct WebError {
message: String,
}
impl WebError {
fn new(message: String) -> Self {
Self { message }
}
}

View file

@ -2,27 +2,18 @@ use actix_cors::Cors;
use actix_web::{App, HttpServer, web}; use actix_web::{App, HttpServer, web};
use argon2::Argon2; use argon2::Argon2;
use clap::Parser; use clap::Parser;
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
use diesel_async::pooled_connection::deadpool::Pool;
use error::Error;
use objects::MailClient;
use simple_logger::SimpleLogger; use simple_logger::SimpleLogger;
use sqlx::{PgPool, Pool, Postgres};
use std::time::SystemTime; use std::time::SystemTime;
mod config; mod config;
use config::{Config, ConfigBuilder}; use config::{Config, ConfigBuilder};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
type Conn =
deadpool::managed::Object<AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>>;
mod api; mod api;
pub mod error;
pub mod objects; pub mod structs;
pub mod schema;
pub mod utils; pub mod utils;
type Error = Box<dyn std::error::Error>;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
struct Args { struct Args {
@ -32,16 +23,11 @@ struct Args {
#[derive(Clone)] #[derive(Clone)]
pub struct Data { pub struct Data {
pub pool: deadpool::managed::Pool< pub pool: Pool<Postgres>,
AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>,
Conn,
>,
pub cache_pool: redis::Client, pub cache_pool: redis::Client,
pub config: Config, pub _config: Config,
pub argon2: Argon2<'static>, pub argon2: Argon2<'static>,
pub start_time: SystemTime, pub start_time: SystemTime,
pub bunny_storage: bunny_api_tokio::EdgeStorageClient,
pub mail_client: MailClient,
} }
#[tokio::main] #[tokio::main]
@ -58,40 +44,105 @@ async fn main() -> Result<(), Error> {
let web = config.web.clone(); let web = config.web.clone();
// create a new connection pool with the default config let pool = PgPool::connect_with(config.database.connect_options()).await?;
let pool_config =
AsyncDieselConnectionManager::<diesel_async::AsyncPgConnection>::new(config.database.url());
let pool = Pool::builder(pool_config).build()?;
let cache_pool = redis::Client::open(config.cache_database.url())?; let cache_pool = redis::Client::open(config.cache_database.url())?;
let bunny = config.bunny.clone(); /*
TODO: Figure out if a table should be used here and if not then what.
Also figure out if these should be different types from what they currently are and if we should add more "constraints"
let bunny_storage = bunny_api_tokio::EdgeStorageClient::new(bunny.api_key, bunny.endpoint, bunny.storage_zone).await?; TODO: References to time should be removed in favor of using the timestamp built in to UUIDv7 (apart from deleted_at in users)
*/
let mail = config.mail.clone(); sqlx::raw_sql(
r#"
let mail_client = MailClient::new( CREATE TABLE IF NOT EXISTS users (
mail.smtp.credentials(), uuid uuid PRIMARY KEY NOT NULL,
mail.smtp.server, username varchar(32) NOT NULL,
mail.address, display_name varchar(64) DEFAULT NULL,
mail.tls, password varchar(512) NOT NULL,
)?; email varchar(100) NOT NULL,
email_verified boolean NOT NULL DEFAULT FALSE,
let database_url = config.database.url(); is_deleted boolean NOT NULL DEFAULT FALSE,
deleted_at int8 DEFAULT NULL,
tokio::task::spawn_blocking(move || { CONSTRAINT unique_username_active UNIQUE NULLS NOT DISTINCT (username, is_deleted),
use diesel::prelude::Connection; CONSTRAINT unique_email_active UNIQUE NULLS NOT DISTINCT (email, is_deleted)
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper; );
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_username_active
let mut conn = ON users(username)
AsyncConnectionWrapper::<diesel_async::AsyncPgConnection>::establish(&database_url)?; WHERE is_deleted = FALSE;
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_email_active
conn.run_pending_migrations(MIGRATIONS)?; ON users(email)
Ok::<_, Box<dyn std::error::Error + Send + Sync>>(()) WHERE is_deleted = FALSE;
}) CREATE TABLE IF NOT EXISTS instance_permissions (
.await? uuid uuid NOT NULL REFERENCES users(uuid),
.unwrap(); administrator boolean NOT NULL DEFAULT FALSE
);
CREATE TABLE IF NOT EXISTS refresh_tokens (
token varchar(64) PRIMARY KEY UNIQUE NOT NULL,
uuid uuid NOT NULL REFERENCES users(uuid),
created_at int8 NOT NULL,
device_name varchar(16) NOT NULL
);
CREATE TABLE IF NOT EXISTS access_tokens (
token varchar(32) PRIMARY KEY UNIQUE NOT NULL,
refresh_token varchar(64) UNIQUE NOT NULL REFERENCES refresh_tokens(token) ON UPDATE CASCADE ON DELETE CASCADE,
uuid uuid NOT NULL REFERENCES users(uuid),
created_at int8 NOT NULL
);
CREATE TABLE IF NOT EXISTS guilds (
uuid uuid PRIMARY KEY NOT NULL,
owner_uuid uuid NOT NULL REFERENCES users(uuid),
name VARCHAR(100) NOT NULL,
description VARCHAR(300)
);
CREATE TABLE IF NOT EXISTS guild_members (
uuid uuid PRIMARY KEY NOT NULL,
guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE,
user_uuid uuid NOT NULL REFERENCES users(uuid),
nickname VARCHAR(100) DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS roles (
uuid uuid UNIQUE NOT NULL,
guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE,
name VARCHAR(50) NOT NULL,
color int NOT NULL DEFAULT 16777215,
position int NOT NULL,
permissions int8 NOT NULL DEFAULT 0,
PRIMARY KEY (uuid, guild_uuid)
);
CREATE TABLE IF NOT EXISTS role_members (
role_uuid uuid NOT NULL REFERENCES roles(uuid) ON DELETE CASCADE,
member_uuid uuid NOT NULL REFERENCES guild_members(uuid) ON DELETE CASCADE,
PRIMARY KEY (role_uuid, member_uuid)
);
CREATE TABLE IF NOT EXISTS channels (
uuid uuid PRIMARY KEY NOT NULL,
guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE,
name varchar(32) NOT NULL,
description varchar(500) NOT NULL
);
CREATE TABLE IF NOT EXISTS channel_permissions (
channel_uuid uuid NOT NULL REFERENCES channels(uuid) ON DELETE CASCADE,
role_uuid uuid NOT NULL REFERENCES roles(uuid) ON DELETE CASCADE,
permissions int8 NOT NULL DEFAULT 0,
PRIMARY KEY (channel_uuid, role_uuid)
);
CREATE TABLE IF NOT EXISTS messages (
uuid uuid PRIMARY KEY NOT NULL,
channel_uuid uuid NOT NULL REFERENCES channels(uuid) ON DELETE CASCADE,
user_uuid uuid NOT NULL REFERENCES users(uuid),
message varchar(4000) NOT NULL
);
CREATE TABLE IF NOT EXISTS invites (
id varchar(32) PRIMARY KEY NOT NULL,
guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE,
user_uuid uuid NOT NULL REFERENCES users(uuid)
);
"#,
)
.execute(&pool)
.await?;
/* /*
**Stored for later possible use** **Stored for later possible use**
@ -113,12 +164,10 @@ async fn main() -> Result<(), Error> {
let data = Data { let data = Data {
pool, pool,
cache_pool, cache_pool,
config, _config: config,
// TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk) // TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk)
argon2: Argon2::default(), argon2: Argon2::default(),
start_time: SystemTime::now(), start_time: SystemTime::now(),
bunny_storage,
mail_client,
}; };
HttpServer::new(move || { HttpServer::new(move || {
@ -150,9 +199,9 @@ async fn main() -> Result<(), Error> {
App::new() App::new()
.app_data(web::Data::new(data.clone())) .app_data(web::Data::new(data.clone()))
.wrap(cors) .wrap(cors)
.service(api::web(data.config.web.backend_url.path())) .service(api::web())
}) })
.bind((web.ip, web.port))? .bind((web.url, web.port))?
.run() .run()
.await?; .await?;

View file

@ -1,384 +0,0 @@
use diesel::{
ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete,
insert_into, update,
};
use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
Conn, Data,
error::Error,
schema::{channel_permissions, channels, messages},
utils::{CHANNEL_REGEX, 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<String>,
is_above: Option<Uuid>,
}
impl ChannelBuilder {
async fn build(self, conn: &mut Conn) -> Result<Channel, Error> {
use self::channel_permissions::dsl::*;
let channel_permission: Vec<ChannelPermission> = 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<String>,
pub is_above: Option<Uuid>,
pub permissions: Vec<ChannelPermission>,
}
#[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(
pool: &deadpool::managed::Pool<
AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>,
Conn,
>,
guild_uuid: Uuid,
) -> Result<Vec<Self>, Error> {
let mut conn = pool.get().await?;
use channels::dsl;
let channel_builders: Vec<ChannelBuilder> = 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(data: &Data, channel_uuid: Uuid) -> Result<Self, Error> {
if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await {
return Ok(serde_json::from_str(&cache_hit)?);
}
let mut conn = data.pool.get().await?;
use channels::dsl;
let channel_builder: ChannelBuilder = dsl::channels
.filter(dsl::uuid.eq(channel_uuid))
.select(ChannelBuilder::as_select())
.get_result(&mut conn)
.await?;
let channel = channel_builder.build(&mut conn).await?;
data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60)
.await?;
Ok(channel)
}
pub async fn new(
data: actix_web::web::Data<Data>,
guild_uuid: Uuid,
name: String,
description: Option<String>,
) -> Result<Self, Error> {
if !CHANNEL_REGEX.is_match(&name) {
return Err(Error::BadRequest("Channel name is invalid".to_string()));
}
let mut conn = data.pool.get().await?;
let channel_uuid = Uuid::now_v7();
let channels = Self::fetch_all(&data.pool, 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(&mut 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(&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,
is_above: None,
permissions: vec![],
};
data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800)
.await?;
if data
.get_cache_key(format!("{}_channels", guild_uuid))
.await
.is_ok()
{
data.del_cache_key(format!("{}_channels", guild_uuid))
.await?;
}
Ok(channel)
}
pub async fn delete(self, data: &Data) -> Result<(), Error> {
let mut conn = data.pool.get().await?;
use channels::dsl;
match update(channels::table)
.filter(dsl::is_above.eq(self.uuid))
.set(dsl::is_above.eq(None::<Uuid>))
.execute(&mut 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(&mut conn)
.await?;
match update(channels::table)
.filter(dsl::is_above.eq(self.uuid))
.set(dsl::is_above.eq(self.is_above))
.execute(&mut conn)
.await
{
Ok(r) => Ok(r),
Err(diesel::result::Error::NotFound) => Ok(0),
Err(e) => Err(e),
}?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() {
data.del_cache_key(self.uuid.to_string()).await?;
}
if data
.get_cache_key(format!("{}_channels", self.guild_uuid))
.await
.is_ok()
{
data.del_cache_key(format!("{}_channels", self.guild_uuid))
.await?;
}
Ok(())
}
pub async fn fetch_messages(
&self,
data: &Data,
amount: i64,
offset: i64,
) -> Result<Vec<Message>, Error> {
let mut conn = data.pool.get().await?;
use messages::dsl;
let messages: Vec<MessageBuilder> = 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(&mut conn)
.await,
)?;
let message_futures = messages.iter().map(async move |b| b.build(data).await);
futures::future::try_join_all(message_futures).await
}
pub async fn new_message(
&self,
data: &Data,
user_uuid: Uuid,
message: String,
) -> Result<Message, Error> {
let message_uuid = Uuid::now_v7();
let message = MessageBuilder {
uuid: message_uuid,
channel_uuid: self.uuid,
user_uuid,
message,
};
let mut conn = data.pool.get().await?;
insert_into(messages::table)
.values(message.clone())
.execute(&mut conn)
.await?;
message.build(data).await
}
pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> {
if !CHANNEL_REGEX.is_match(&new_name) {
return Err(Error::BadRequest("Channel name is invalid".to_string()));
}
let mut conn = data.pool.get().await?;
use channels::dsl;
update(channels::table)
.filter(dsl::uuid.eq(self.uuid))
.set(dsl::name.eq(&new_name))
.execute(&mut conn)
.await?;
self.name = new_name;
Ok(())
}
pub async fn set_description(
&mut self,
data: &Data,
new_description: String,
) -> Result<(), Error> {
let mut conn = data.pool.get().await?;
use channels::dsl;
update(channels::table)
.filter(dsl::uuid.eq(self.uuid))
.set(dsl::description.eq(&new_description))
.execute(&mut conn)
.await?;
self.description = Some(new_description);
Ok(())
}
pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> {
let mut conn = data.pool.get().await?;
use channels::dsl;
let old_above_uuid: Option<Uuid> = match dsl::channels
.filter(dsl::is_above.eq(self.uuid))
.select(dsl::uuid)
.get_result(&mut 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::<Uuid>))
.execute(&mut conn)
.await?;
}
match update(channels::table)
.filter(dsl::is_above.eq(new_is_above))
.set(dsl::is_above.eq(self.uuid))
.execute(&mut 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(&mut 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(&mut conn)
.await?;
}
self.is_above = Some(new_is_above);
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show more