Stripped out ui/api

This commit is contained in:
Benjamin Sherriff
2024-09-03 22:32:43 -04:00
committed by Benjamin Sherriff
parent c83d398ce0
commit 96fe3fc0e5
152 changed files with 110 additions and 10056 deletions

View File

@@ -6,7 +6,6 @@ DATABASE_NAME=siren
DATABASE_HOST=localhost DATABASE_HOST=localhost
DATABASE_PORT=5432 DATABASE_PORT=5432
KEYS_DIR_PATH= # OPTIONAL
SESSION_TTL=1440 SESSION_TTL=1440
MINIO_ROOT_USER=siren MINIO_ROOT_USER=siren

1
.version Normal file
View File

@@ -0,0 +1 @@
SIREN_VERSION=0.2.7

View File

@@ -1,6 +1,6 @@
{ {
"rust-analyzer.linkedProjects": [ "rust-analyzer.linkedProjects": [
"./service/Cargo.toml" "./Cargo.toml"
], ],
"rust-analyzer.showUnlinkedFileNotification": false "rust-analyzer.showUnlinkedFileNotification": false
} }

View File

@@ -1,24 +1,22 @@
#!make #!make
SHELL := /bin/bash SHELL := /bin/bash
GIT_HASH ?= $(shell git log --format="%h" -n 1)
export VERSION=$(if $(v),$(v),latest)
include .env include .env
-include .env.local -include .env.local
export export
.PHONY: help build test up down exec clean .PHONY: help
help: ## Help command help: ## Help command
@echo @echo
@cat Makefile | grep -E '^[a-zA-Z\/_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @cat Makefile | grep -E '^[a-zA-Z\/_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
@echo @echo
up: ## Start the backend containers backend-up: ## Start the backend containers
@docker-compose --profile backend up -d @docker compose --profile backend up -d
down: ## Stop the backend containers backend-down: ## Stop the backend containers
@docker-compose --profile backend down @docker compose --profile backend down
run: ## Run the project run: ## Run the project
@echo "Running project..." @echo "Running project..."
@@ -36,17 +34,17 @@ clean: ## Clean the project
@echo "Clean complete" @echo "Clean complete"
docker-up: ## Start the app docker-up: ## Start the app
@docker-compose --profile backend --profile siren up -d @docker compose --profile backend --profile siren up -d
docker-down: ## Stop the app docker-down: ## Stop the app
@docker-compose --profile backend --profile siren down @docker compose --profile backend --profile siren down
docker-build: ## Build the docker image docker-build: ## Build the docker image
@docker-compose build @docker compose build
docker-clean: ## Stop the docker containers and remove volumes docker-clean: ## Stop the docker containers and remove volumes
@echo "Stopping docker container and removing volumes..." @echo "Stopping docker container and removing volumes..."
@docker-compose --profile backend --profile siren down -v @docker compose --profile backend --profile siren down -v
@echo "Docker container stopped and volumes removed" @echo "Docker container stopped and volumes removed"
refresh: docker-clean up ## Refresh the docker containers docker-refresh: docker-clean backend-up ## Refresh the docker containers

82
docker-compose.yml Normal file
View File

@@ -0,0 +1,82 @@
x-env_file: &env
- path: .env
required: true
- path: .env.local
required: false
name: siren
services:
# bot:
# image: siren-service:${SIREN_VERSION:-latest}
# container_name: siren-service
# build:
# context: .
# dockerfile: ./Dockerfile
# args:
# - VERSION=${SIREN_VERSION:-latest}
# env_file: *env
# environment:
# DATABASE_HOST: db
# DATABASE_PORT: 5432
# REDIS_HOST: redis
# REDIS_PORT: 6379
# MINIO_HOST: minio
# MINIO_PORT: 9000
# SERVICE_HOST: service
# SERVICE_PORT: 5000
# DATA_DIR_PATH: /data
# volumes:
# - ${DATA_DIR_PATH:-/data}:/data
# ports:
# - ${SERVICE_PORT:-5000}:5000
# depends_on:
# - db
# - redis
# - minio
# networks:
# - frontend
# - backend
# restart: unless-stopped
# profiles:
# - bot
db:
image: postgres:latest
container_name: siren-db
env_file: *env
environment:
POSTGRES_USER: ${DATABASE_USER}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
volumes:
- db:/var/lib/postgresql/data
- db_logs:/var/log
ports:
- ${DATABASE_PORT:-5432}:5432
networks:
- backend
profiles:
- backend
restart: unless-stopped
redis:
image: redis:latest
container_name: siren-redis
volumes:
- redis:/data
ports:
- ${REDIS_PORT:-6379}:6379
networks:
- backend
profiles:
- backend
restart: unless-stopped
volumes:
db:
db_logs:
redis:
networks:
frontend:
backend:

View File

@@ -1 +0,0 @@
SIREN_VERSION=0.2.6

View File

@@ -1,102 +0,0 @@
x-env_file: &env
- path: .env
required: true
- path: .env.local
required: false
name: siren
services:
service:
image: siren-service:${SIREN_VERSION:-latest}
container_name: siren-service
build:
context: .
dockerfile: ./Dockerfile
args:
- VERSION=${SIREN_VERSION:-latest}
env_file: *env
environment:
DATABASE_HOST: db
DATABASE_PORT: 5432
REDIS_HOST: redis
REDIS_PORT: 6379
MINIO_HOST: minio
MINIO_PORT: 9000
SERVICE_HOST: service
SERVICE_PORT: 5000
DATA_DIR_PATH: /data
KEYS_DIR_PATH: /keys
volumes:
- ${DATA_DIR_PATH:-~/data}:/data
ports:
- ${SERVICE_PORT:-5000}:5000
depends_on:
- db
- redis
- minio
networks:
- frontend
- backend
restart: unless-stopped
profiles:
- siren
db:
image: postgres:latest
container_name: siren-db
env_file: *env
environment:
POSTGRES_USER: ${DATABASE_USER}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
volumes:
- db:/var/lib/postgresql/data
- db_logs:/var/log
ports:
- ${DATABASE_PORT:-5432}:5432
networks:
- backend
restart: unless-stopped
profiles:
- backend
redis:
image: redis:latest
container_name: siren-redis
volumes:
- redis:/data
ports:
- ${REDIS_PORT:-6379}:6379
networks:
- backend
restart: unless-stopped
profiles:
- backend
minio:
image: minio/minio
container_name: siren-minio
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- minio:/data
ports:
- ${MINIO_PORT:-9000}:9000
- ${MINIO_PORT_INTERNAL:-9001}:9001
networks:
- backend
command: server --console-address ":9001" /data
restart: unless-stopped
profiles:
- backend
volumes:
db:
db_logs:
redis:
minio:
networks:
frontend:
backend:

View File

@@ -1,7 +0,0 @@
mod model;
mod routes;
mod session;
pub use model::*;
pub use session::*;
pub use routes::init_routes;

View File

@@ -1,174 +0,0 @@
use std::future::{ready, Ready};
use actix_web::{FromRequest, Error as ActixError, HttpRequest, dev::Payload, http};
use diesel::prelude::*;
use serde::{Serialize, Deserialize};
use siren::ServiceError;
use crate::storage::{schema::users, connection};
use super::{hash, Session, SESSION_COOKIE_NAME};
#[derive(Debug, Serialize, Deserialize)]
pub struct RegisterUser {
pub email: String,
pub password: String,
pub first_name: String,
pub last_name: String,
}
impl RegisterUser {
pub fn convert_to_insert(self) -> Result<InsertUser, ServiceError> {
Ok(InsertUser {
email: self.email.to_lowercase(),
hash: hash(&self.password)?,
role: "user".to_string(),
first_name: self.first_name,
last_name: self.last_name,
updated_at: chrono::Utc::now().naive_utc(),
created_at: chrono::Utc::now().naive_utc(),
profile_picture: None,
verified: false,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
}
#[derive(Debug, Queryable, QueryableByName, Serialize, Deserialize)]
#[diesel(table_name = users)]
pub struct QueryUser {
pub email: String,
pub hash: String,
pub role: String,
pub first_name: String,
pub last_name: String,
pub updated_at: chrono::NaiveDateTime,
pub created_at: chrono::NaiveDateTime,
pub profile_picture: Option<String>,
pub verified: bool,
}
impl QueryUser {
pub fn get_by_email(email: &str) -> Result<QueryUser, ServiceError> {
let mut conn = connection()?;
// Check if the user exists by email, case insensitive
let user = users::table
.filter(users::email.eq(email.to_lowercase()))
.first(&mut conn)?;
Ok(user)
}
}
#[derive(Debug, Insertable, AsChangeset, Serialize, Deserialize)]
#[diesel(table_name = users)]
pub struct InsertUser {
pub email: String,
pub hash: String,
pub role: String,
pub first_name: String,
pub last_name: String,
pub updated_at: chrono::NaiveDateTime,
pub created_at: chrono::NaiveDateTime,
pub profile_picture: Option<String>,
pub verified: bool,
}
impl InsertUser {
pub fn insert(user: Self) -> Result<QueryUser, ServiceError> {
let mut conn = connection()?;
let user = diesel::insert_into(users::table)
.values(user)
.get_result(&mut conn)?;
Ok(user)
}
pub fn update_profile(
email: &str,
profile_picture: Option<&str>,
) -> Result<QueryUser, ServiceError> {
let mut conn = connection()?;
let user = diesel::update(users::table)
.filter(users::email.eq(&email))
.set(users::profile_picture.eq(profile_picture))
.get_result(&mut conn)?;
Ok(user)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResponseUser {
pub email: String,
pub role: String,
pub first_name: String,
pub last_name: String,
pub profile_picture: Option<String>,
}
impl From<QueryUser> for ResponseUser {
fn from(user: QueryUser) -> Self {
ResponseUser {
email: user.email,
role: user.role,
first_name: user.first_name,
last_name: user.last_name,
profile_picture: user.profile_picture,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Auth {
pub id: String,
pub user: ResponseUser,
}
impl FromRequest for Auth {
type Error = ActixError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let session_id = match req
.cookie(SESSION_COOKIE_NAME)
.map(|c| c.value().to_string())
.or_else(|| {
req
.headers()
.get(http::header::AUTHORIZATION)
.map(|h| h.to_str().unwrap().split_at(7).1.to_string())
}) {
Some(token) => token,
None => {
return ready(Err(ActixError::from(ServiceError {
status: 401,
message: "Unauthorized".to_string(),
})))
}
};
let ip_address = req.peer_addr().unwrap().ip().to_string();
match Session::verify(&session_id, &ip_address) {
Ok(v) => {
return ready(Ok(Auth {
id: v.0.id,
user: v.1.into(),
}))
}
Err(err) => return ready(Err(ActixError::from(err))),
}
}
}
pub fn verify_role(auth: &Auth, role: &str) -> Result<(), ServiceError> {
if auth.user.role == role {
Ok(())
} else {
Err(ServiceError {
status: 403,
message: "Forbidden".to_string(),
})
}
}

View File

@@ -1,226 +0,0 @@
use std::env;
use actix_web::{
get, post, web, HttpResponse, ResponseError,
cookie::{Cookie, time::Duration},
HttpRequest,
};
use log::error;
use redis::AsyncCommands;
use siren::ServiceError;
use crate::{
auth::{InsertUser, Auth, LoginRequest, QueryUser, RegisterUser, Session, SESSION_COOKIE_NAME},
storage::{self},
};
use super::verify_hash;
#[post("/register")]
async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
let register_user = user.0;
let insert_user: InsertUser = match register_user.convert_to_insert() {
Ok(user) => user,
Err(err) => return ResponseError::error_response(&err),
};
match InsertUser::insert(insert_user) {
Ok(_) => HttpResponse::Created().finish(),
Err(err) => {
// Obfuscate the service error message to prevent leaking database details
if err.status == 409 {
return HttpResponse::Conflict().finish();
} else {
return ResponseError::error_response(&err);
}
}
}
}
#[post("/login")]
async fn login(request: HttpRequest, login_request: web::Json<LoginRequest>) -> HttpResponse {
let email = login_request.email.clone();
// Get IP address
let ip_address = request.peer_addr().unwrap().ip().to_string();
let query_user = match QueryUser::get_by_email(&email) {
Ok(query_user) => query_user,
Err(_) => {
return ResponseError::error_response(&ServiceError {
status: 401,
message: "The email or password was incorrect.".to_string(),
})
}
};
// Verify password
if verify_hash(&login_request.password, &query_user.hash) {
let session = Session::new(&email, &ip_address);
let mut conn = match storage::redis_async_connection().await {
Ok(conn) => conn,
Err(err) => {
error!("Failed to get redis connection: {}", err);
return ResponseError::error_response(&err);
}
};
let session_ttl = env::var("SESSION_TTL")
.expect("SESSION_TTL must be set")
.parse::<i64>()
.expect("SESSION_TTL must be an integer");
let session_result: redis::RedisResult<()> = conn
.set_ex(
session.id.to_string(),
&serde_json::to_string(&session).unwrap(),
(session_ttl * 60) as usize,
)
.await;
if let Err(err) = session_result {
error!("Failed to set access token in redis: {}", err);
return ResponseError::error_response(&ServiceError::from(err));
};
let session_cookie = Cookie::build(SESSION_COOKIE_NAME, session.id.clone())
.path("/")
.max_age(Duration::new(session_ttl * 60, 0))
.http_only(true)
.secure(true)
.finish();
let user_id_cookie = Cookie::build("user_id", session.user_id.clone())
.path("/")
.max_age(Duration::new(session_ttl * 60, 0))
.http_only(false)
.finish();
HttpResponse::Ok()
.cookie(session_cookie)
.cookie(user_id_cookie)
.json(Auth {
id: session.id,
user: query_user.into(),
})
} else {
return ResponseError::error_response(&ServiceError {
status: 401,
message: "The email or password was incorrect.".to_string(),
});
}
}
#[get("/refresh")]
async fn refresh(req: HttpRequest, auth: Auth) -> HttpResponse {
let ip_address = req.peer_addr().unwrap().ip().to_string();
let mut conn = match storage::redis_async_connection().await {
Ok(conn) => conn,
Err(err) => {
error!("Failed to get redis connection: {}", err);
return ResponseError::error_response(&err);
}
};
let session_ttl = env::var("SESSION_TTL")
.expect("SESSION_TTL must be set")
.parse::<i64>()
.expect("SESSION_TTL must be an integer");
// Delete old session
let _: redis::RedisResult<()> = conn.del(auth.id).await;
// Create new session
let session = Session::new(&auth.user.email, &ip_address);
let session_result: redis::RedisResult<()> = conn
.set_ex(
session.id.to_string(),
&serde_json::to_string(&session).unwrap(),
(session_ttl * 60) as usize,
)
.await;
if let Err(err) = session_result {
error!("Failed to set session id in redis: {}", err);
return ResponseError::error_response(&ServiceError::from(err));
};
// Create cookies
let session_cookie = session.create_cookie();
let user_id_cookie = Cookie::build("user_id", session.user_id.clone())
.path("/")
.max_age(Duration::new(session_ttl * 60, 0))
.http_only(false)
.finish();
HttpResponse::Ok()
.cookie(session_cookie)
.cookie(user_id_cookie)
.json(Auth {
id: session.id,
user: auth.user,
})
}
#[post("/logout")]
async fn logout(auth: Auth) -> HttpResponse {
let mut conn = match storage::redis_async_connection().await {
Ok(conn) => conn,
Err(err) => {
error!("Failed to get redis connection: {}", err);
return ResponseError::error_response(&err);
}
};
let session_result: redis::RedisResult<()> = conn.del(&auth.id.to_string()).await;
if let Err(err) = session_result {
error!("Failed to remove session id in redis: {}", err);
return ResponseError::error_response(&ServiceError::from(err));
};
let session_cookie = Cookie::build(SESSION_COOKIE_NAME, "")
.path("/")
.max_age(Duration::new(-1, 0))
.secure(true)
.http_only(true)
.finish();
let user_id_cookie = Cookie::build("user_id", "")
.path("/")
.max_age(Duration::new(-1, 0))
.http_only(true)
.finish();
HttpResponse::Ok()
.cookie(session_cookie)
.cookie(user_id_cookie)
.finish()
}
#[get("/me")]
async fn me(auth: Auth) -> HttpResponse {
HttpResponse::Ok().json(auth)
}
#[get("/roles")]
async fn roles() -> HttpResponse {
HttpResponse::Ok().json(vec!["admin", "user"])
}
pub fn init_routes(config: &mut web::ServiceConfig) {
// TODO: Remove this when deploying
let r = RegisterUser {
email: "admin".to_string(),
password: "admin".to_string(),
first_name: "Admin".to_string(),
last_name: "Admin".to_string(),
};
let mut u = r.convert_to_insert().unwrap();
u.role = "admin".to_string();
u.verified = true;
let _ = InsertUser::insert(u);
config.service(
web::scope("auth")
.service(register)
.service(login)
.service(refresh)
.service(logout)
.service(me)
.service(roles),
);
}

View File

@@ -1,109 +0,0 @@
use std::env;
use actix_web::cookie::{time::Duration, Cookie};
use argon2::{
password_hash::{rand_core::OsRng, SaltString},
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
};
use rand::prelude::*;
use rand_chacha::ChaCha20Rng;
use redis::Commands;
use serde::{Deserialize, Serialize};
use siren::ServiceError;
use super::QueryUser;
pub const SESSION_COOKIE_NAME: &str = "session";
#[derive(Debug, Serialize, Deserialize)]
pub struct Session {
pub id: String,
pub user_id: String,
pub ip_address: String,
pub expiration: i64,
}
impl Session {
pub fn new(user_id: &str, ip_address: &str) -> Self {
let ttl = env::var("SESSION_TTL")
.expect("SESSION_TTL must be set")
.parse::<i64>()
.expect("SESSION_TTL must be an integer");
let now = chrono::Utc::now();
Self {
id: csprng_128bit(),
user_id: user_id.to_string(),
ip_address: hash(&ip_address).unwrap(),
expiration: (now + chrono::Duration::minutes(ttl)).timestamp(),
}
}
pub fn verify(session_id: &str, ip_address: &str) -> Result<(Self, QueryUser), ServiceError> {
let mut conn = crate::storage::redis_connection()?;
// Check if the session exists
let session = match conn.get::<_, String>(session_id) {
Ok(session) => session,
Err(_) => return Err(ServiceError::new(401, "Unauthorized".to_string())),
};
let session: Self = serde_json::from_str(&session)?;
// Check if the IP address matches
let session_ip_address = session.ip_address.clone();
let session_user_id = session.user_id.clone();
if verify_hash(ip_address, &session_ip_address) {
let email = session_user_id;
// Check if the user exists
let user = match crate::auth::model::QueryUser::get_by_email(&email) {
Ok(user) => user,
Err(_) => return Err(ServiceError::new(401, "Unauthorized".to_string())),
};
// Check if the session has expired
let now = chrono::Utc::now().timestamp();
if now < session.expiration {
return Ok((session, user));
}
}
Err(ServiceError::new(401, "Unauthorized".to_string()))
}
pub fn create_cookie(&self) -> Cookie {
let ttl = env::var("SESSION_TTL")
.expect("SESSION_TTL must be set")
.parse::<i64>()
.expect("SESSION_TTL must be an integer");
Cookie::build(SESSION_COOKIE_NAME, self.id.clone())
.path("/")
.max_age(Duration::new(ttl * 60, 0))
.secure(true)
.http_only(true)
.finish()
}
}
fn csprng_128bit() -> String {
// Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9)
let rng = ChaCha20Rng::from_entropy();
rng
.sample_iter(rand::distributions::Alphanumeric)
.take(16)
.map(char::from)
.collect()
}
pub fn hash(str: &str) -> Result<String, ServiceError> {
let salt = SaltString::generate(&mut OsRng);
let bytes = str.as_bytes();
let hash = Argon2::default().hash_password(bytes, &salt)?.to_string();
Ok(hash)
}
pub fn verify_hash(str: &str, hash: &str) -> bool {
let bytes = str.as_bytes();
let parsed_hash = match PasswordHash::new(hash) {
Ok(h) => h,
Err(_) => return false,
};
match Argon2::default().verify_password(bytes, &parsed_hash) {
Ok(_) => true,
Err(_) => false,
}
}

View File

@@ -1,123 +0,0 @@
use std::env;
use argon2::{password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
use rand::prelude::*;
use rand_chacha::ChaCha20Rng;
use serde::{Deserialize, Serialize};
use siren::ServiceError;
#[derive(Debug, Serialize, Deserialize)]
struct TokenClaims {
sub: String, // Subject (User)
id: String, // Access Token ID
iss: String, // Issuer (Service)
exp: i64, // Expiration time
iat: i64, // Issued At
nbf: i64 // Not Before
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AccessToken {
pub token: Option<String>, // Access Token
pub id: String, // Access Token ID
pub email: String, // Subject (User)
pub expiration: Option<i64> // Expiration time
}
impl AccessToken {
fn new(email: &str) -> Result<Self, ServiceError> {
let ttl = env::var("ACCESS_TOKEN_MAXAGE")
.expect("ACCESS_TOKEN_MAXAGE must be set")
.parse::<i64>()
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
let keys_dir = env::var("KEYS_DIR_PATH")?;
let private_key = std::fs::read_to_string(format!("{}/private_key.pem", keys_dir))?;
let now = chrono::Utc::now();
let mut token_details = Self {
token: None,
id: csprng_128bit(),
email: email.to_string(),
expiration: Some((now + chrono::Duration::minutes(ttl)).timestamp())
};
let claims = TokenClaims {
sub: token_details.email.clone(),
iss: "siren".to_string(),
id: token_details.id.to_string(),
exp: token_details.expiration.unwrap(),
iat: now.timestamp(),
nbf: now.timestamp()
};
let header = Header::new(Algorithm::RS256);
let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?;
let token = encode(&header, &claims, &key)?;
token_details.token = Some(token);
Ok(token_details)
}
pub fn decode(token: &str, public_key: &str) -> Result<Self, ServiceError> {
let key = DecodingKey::from_rsa_pem(public_key.as_bytes())?;
let validation = Validation::new(Algorithm::RS256);
let decoded = decode::<TokenClaims>(token, &key, &validation)?;
let email = decoded.claims.sub;
let id = decoded.claims.id;
Ok(Self { token: None, id, email, expiration: None })
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RefreshToken {
pub id: String,
pub email: String,
pub ip_address: String,
pub tokens: Vec<String>,
pub expiration: i64,
}
impl RefreshToken {
pub fn new(email: &str, ip_address: &str) -> Self {
let ttl = env::var("REFRESH_TOKEN_MAXAGE")
.expect("REFRESH_TOKEN_MAXAGE must be set")
.parse::<i64>()
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
let now = chrono::Utc::now();
Self {
id: csprng_128bit(),
email: email.to_string(),
ip_address: hash(&ip_address).unwrap(),
tokens: vec![],
expiration: (now + chrono::Duration::minutes(ttl)).timestamp()
}
}
pub fn create_access_token(&mut self) -> Result<AccessToken, ServiceError> {
let access_token = AccessToken::new(&self.email)?;
self.tokens.push(access_token.id.clone());
Ok(access_token)
}
}
fn csprng_128bit() -> String {
// Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9)
let rng = ChaCha20Rng::from_entropy();
rng.sample_iter(rand::distributions::Alphanumeric).take(16).map(char::from).collect()
}
pub fn hash(str: &str) -> Result<String, ServiceError> {
let salt = SaltString::generate(&mut OsRng);
let bytes = str.as_bytes();
let hash = Argon2::default().hash_password(bytes, &salt)?.to_string();
Ok(hash)
}
pub fn verify_hash(str: &str, hash: &str) -> bool {
let bytes = str.as_bytes();
let parsed_hash = match PasswordHash::new(hash) {
Ok(h) => h,
Err(_) => return false
};
match Argon2::default().verify_password(bytes, &parsed_hash) {
Ok(_) => true,
Err(_) => false
}
}

View File

@@ -1,5 +0,0 @@
mod model;
mod routes;
pub use model::*;
pub use routes::init_routes;

View File

@@ -1,443 +0,0 @@
use std::{sync::Arc, pin::Pin};
use actix_web::{get, post, web, HttpResponse, ResponseError};
use serde::{Serialize, Deserialize};
use serenity::model::prelude::{GuildChannel, ChannelType};
use siren::{ServiceError, Response};
use crate::{
AppState,
bot::commands::audio::{play::play_track, join},
bot::guilds::QueryGuild,
auth::{Auth, verify_role},
};
#[get("/guilds")]
async fn get_guilds(data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err);
};
let guild_results = &data.http.get_guilds(None, None).await;
let guilds = match guild_results {
Ok(guilds) => guilds,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
HttpResponse::Ok().json(Response {
data: guilds,
metadata: None,
})
}
#[get("/{id}/text")]
async fn get_text_channels(
id: web::Path<String>,
data: web::Data<Arc<AppState>>,
auth: Auth,
) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err);
};
let channel_results = &data.http.get_channels(id.parse::<u64>().unwrap()).await;
let channels = match channel_results {
Ok(channels) => channels
.iter()
.filter(|c| c.kind == ChannelType::Text)
.collect::<Vec<&GuildChannel>>(),
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
HttpResponse::Ok().json(Response {
data: channels,
metadata: None,
})
}
#[get("/{id}/voice")]
async fn get_voice_channels(
id: web::Path<String>,
data: web::Data<Arc<AppState>>,
auth: Auth,
) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err);
};
let channel_results = &data.http.get_channels(id.parse::<u64>().unwrap()).await;
let channels = match channel_results {
Ok(channels) => channels
.iter()
.filter(|c| c.kind == ChannelType::Voice)
.collect::<Vec<&GuildChannel>>(),
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
HttpResponse::Ok().json(Response {
data: channels,
metadata: None,
})
}
#[derive(Serialize, Deserialize)]
struct ChannelMessage {
message: String,
}
#[post("/{guild_id}/text/{channel_id}/message")]
async fn send_message(
path: web::Path<(String, String)>,
text: web::Json<ChannelMessage>,
data: web::Data<Arc<AppState>>,
auth: Auth,
) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err);
};
let (guild_id, channel_id) = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
let channel_id = match channel_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
let channel_results = &data.http.get_channels(guild_id).await;
let channels = match channel_results {
Ok(channels) => channels,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
let channel = match channels.iter().find(|c| c.id.0 == channel_id) {
Some(channel) => channel,
None => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: format!("Could not find channel with id {}", channel_id),
})
}
};
if let Err(err) = channel
.say(&Pin::new(&data.http).get_ref(), &text.message)
.await
{
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
});
};
HttpResponse::Ok().finish()
}
#[derive(Serialize, Deserialize)]
struct PlayRequest {
track_url: String,
}
#[post("/{guild_id}/voice/{channel_id}/play")]
async fn play(
path: web::Path<(String, String)>,
play_request: web::Json<PlayRequest>,
data: web::Data<Arc<AppState>>,
auth: Auth,
) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err);
};
let (guild_id, channel_id) = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
let channel_id = match channel_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
let http = Pin::new(&data.http).get_ref();
let guild = match http.get_guild(guild_id).await {
Ok(guild) => guild,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
let channel = match http.get_channel(channel_id).await {
Ok(channel) => channel,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
let manager = Arc::clone(&data.songbird);
match join(Arc::clone(&manager), &guild.id, &channel.id()).await {
Ok(_) => {
match play_track(
Arc::clone(&data.songbird),
guild.id,
play_request.track_url.to_string(),
)
.await
{
Ok(_) => HttpResponse::Ok().finish(),
Err(err) => return ResponseError::error_response(&err),
}
}
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 500,
message: err.to_string(),
})
}
}
}
#[post("/{guild_id}/voice/stop")]
async fn stop(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err);
};
let guild_id = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
if let Some(handler_lock) = data.songbird.get(guild_id) {
let handler = handler_lock.lock().await;
handler.queue().stop();
}
HttpResponse::Ok().finish()
}
#[post("/{guild_id}/voice/resume")]
async fn resume(
path: web::Path<String>,
data: web::Data<Arc<AppState>>,
auth: Auth,
) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err);
};
let guild_id = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
if let Some(handler_lock) = data.songbird.get(guild_id) {
let handler = handler_lock.lock().await;
if let Err(err) = handler.queue().resume() {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
});
}
}
HttpResponse::Ok().finish()
}
#[post("/{guild_id}/voice/pause")]
async fn pause(
path: web::Path<String>,
data: web::Data<Arc<AppState>>,
auth: Auth,
) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err);
};
let guild_id = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
if let Some(handler_lock) = data.songbird.get(guild_id) {
let handler = handler_lock.lock().await;
if let Err(err) = handler.queue().pause() {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
});
}
}
HttpResponse::Ok().finish()
}
#[derive(Serialize, Deserialize)]
struct SetVolume {
volume: String,
}
#[get("/{guild_id}/voice/volume")]
async fn get_volume(path: web::Path<String>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err);
};
let guild_id = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
let volume = match QueryGuild::get(guild_id as i64) {
Ok(guild) => guild.volume,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
HttpResponse::Ok().json(volume)
}
#[post("/{guild_id}/voice/volume")]
async fn set_volume(
path: web::Path<String>,
volume: web::Json<SetVolume>,
data: web::Data<Arc<AppState>>,
auth: Auth,
) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err);
};
let guild_id = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
let volume = volume.volume.parse::<i32>().unwrap_or(0);
let manager = Arc::clone(&data.songbird);
let http = Arc::clone(&data.http);
let guild = match http.get_guild(guild_id).await {
Ok(guild) => guild,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
crate::bot::commands::audio::volume::set_volume(manager, guild.id, volume).await;
HttpResponse::Ok().finish()
}
#[post("/{guild_id}/voice/skip")]
async fn skip(path: web::Path<String>, data: web::Data<Arc<AppState>>, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
return ResponseError::error_response(&err);
};
let guild_id = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
if let Some(handler_lock) = data.songbird.get(guild_id) {
let handler = handler_lock.lock().await;
if let Err(err) = handler.queue().skip() {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
});
}
}
HttpResponse::Ok().finish()
}
pub fn init_routes(config: &mut web::ServiceConfig) {
config.service(get_guilds).service(
web::scope("guilds")
.service(get_text_channels)
.service(get_voice_channels)
.service(send_message)
.service(play)
.service(stop)
.service(resume)
.service(pause)
.service(set_volume)
.service(get_volume)
.service(skip),
);
}

View File

@@ -1,5 +0,0 @@
mod model;
mod routes;
pub use model::*;
pub use routes::init_routes;

View File

@@ -1,91 +0,0 @@
use actix_web::{get, post, web, HttpResponse, HttpRequest, ResponseError};
use log::error;
use serde::{Serialize, Deserialize};
use siren::{Response, Metadata, ServiceError};
use crate::{
bot::messages::{QueryMessage, QueryFilters},
auth::{Auth, verify_role},
};
#[derive(Serialize, Deserialize)]
struct GetAllParams {
id: Option<String>,
guild_id: Option<i64>,
channel_id: Option<i64>,
user_id: Option<i64>,
model: Option<String>,
request: Option<String>,
response: Option<String>,
request_tags: Option<Vec<String>>,
response_tags: Option<Vec<String>>,
limit: Option<i32>,
page: Option<i32>,
}
#[get("/messages")]
async fn get_all(req: HttpRequest, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") {
Ok(_) => {}
Err(err) => return ResponseError::error_response(&err),
};
let params = match web::Query::<GetAllParams>::from_query(req.query_string()) {
Ok(params) => params,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
let mut filters = QueryFilters::default();
filters.by_id = params.id.clone();
filters.by_guild_id = params.guild_id;
filters.by_channel_id = params.channel_id;
filters.by_user_id = params.user_id;
filters.by_model = params.model.clone();
filters.by_request = params.request.clone();
filters.by_response = params.response.clone();
filters.by_request_tags = params.request_tags.clone();
filters.by_response_tags = params.response_tags.clone();
let limit = params.limit.unwrap_or(100);
let total_count = QueryMessage::get_count(&filters).unwrap();
let max_page = std::cmp::max((total_count as f64 / limit as f64).ceil() as i32, 1);
let page = std::cmp::min(std::cmp::max(params.page.unwrap_or(1), 1), max_page);
match QueryMessage::get_all(&filters, limit, page) {
Ok(messages) => HttpResponse::Ok().json(Response {
data: messages,
metadata: Some(Metadata {
total: total_count as i32,
limit,
page,
pages: max_page,
}),
}),
Err(err) => {
error!("{:?}", err.message);
ResponseError::error_response(&err)
}
}
}
#[post("/messages")]
async fn create(message: web::Json<QueryMessage>, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") {
Ok(_) => {}
Err(err) => return ResponseError::error_response(&err),
};
match QueryMessage::insert(message.into_inner()) {
Ok(message) => HttpResponse::Created().json(message),
Err(err) => {
error!("{:?}", err.message);
ResponseError::error_response(&err)
}
}
}
pub fn init_routes(config: &mut web::ServiceConfig) {
config.service(get_all);
config.service(create);
}

View File

@@ -1,225 +0,0 @@
use actix_web::{get, post, put, delete, web, HttpResponse, HttpRequest, ResponseError};
use log::error;
use serde::{Serialize, Deserialize};
use siren::{Response, Metadata, ServiceError};
use crate::{
dnd::spells::{QuerySpell, QueryFilters},
auth::{Auth, verify_role},
};
use super::{Spell, InsertSpell};
#[derive(Serialize, Deserialize)]
struct GetAllParams {
name: Option<String>,
like_name: Option<String>,
schools: Option<String>,
levels: Option<String>,
ritual: Option<bool>,
concentration: Option<bool>,
classes: Option<String>,
damage_inflict: Option<String>,
damage_resist: Option<String>,
conditions: Option<String>,
saving_throw: Option<String>,
attack_type: Option<String>,
limit: Option<i32>,
page: Option<i32>,
}
#[get("/spells")]
async fn get_all(req: HttpRequest) -> HttpResponse {
let params = match web::Query::<GetAllParams>::from_query(req.query_string()) {
Ok(params) => params,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
let mut filters = QueryFilters::default();
filters.by_name = params.name.clone();
filters.like_name = params.like_name.clone();
filters.by_schools = match &params.schools {
Some(schools) => Some(schools.split(",").map(|s| s.to_string()).collect()),
None => None,
};
filters.by_levels = match &params.levels {
Some(levels) => Some(
levels
.split(",")
.map(|s| match s.to_string().parse::<i32>() {
Ok(level) => level,
Err(_) => 0,
})
.collect(),
),
None => None,
};
filters.by_ritual = params.ritual;
filters.by_concentration = params.concentration;
filters.by_classes = match &params.classes {
Some(classes) => Some(classes.split(",").map(|s| s.to_string()).collect()),
None => None,
};
filters.by_damage_inflict = match &params.damage_inflict {
Some(damage_inflict) => Some(damage_inflict.split(",").map(|s| s.to_string()).collect()),
None => None,
};
filters.by_damage_resist = match &params.damage_resist {
Some(damage_resist) => Some(damage_resist.split(",").map(|s| s.to_string()).collect()),
None => None,
};
filters.by_conditions = match &params.conditions {
Some(conditions) => Some(conditions.split(",").map(|s| s.to_string()).collect()),
None => None,
};
filters.by_saving_throw = match &params.saving_throw {
Some(saving_throw) => Some(saving_throw.split(",").map(|s| s.to_string()).collect()),
None => None,
};
filters.by_attack_type = match &params.attack_type {
Some(attack_type) => Some(attack_type.split(",").map(|s| s.to_string()).collect()),
None => None,
};
// Limit must be between 1 and 100
let limit = std::cmp::min(std::cmp::max(params.limit.unwrap_or(100), 1), 100);
let total_count = QuerySpell::get_count(&filters).unwrap();
let max_page = std::cmp::max((total_count as f64 / limit as f64).ceil() as i32, 1);
// Page must be between 1 and max_page
let page = std::cmp::min(std::cmp::max(params.page.unwrap_or(1), 1), max_page);
match web::block(move || QuerySpell::get_all(&filters, limit, page))
.await
.unwrap()
{
Ok(spells) => {
let mut response: Vec<Spell> = Vec::new();
for query_spell in spells {
let id = query_spell.id;
let mut spell = Spell::from(query_spell);
spell.id = Some(id);
response.push(spell);
}
HttpResponse::Ok().json(Response {
data: response,
metadata: Some(Metadata {
total: total_count as i32,
limit,
page,
pages: max_page,
}),
})
}
Err(err) => {
error!("{:?}", err.message);
ResponseError::error_response(&err)
}
}
}
#[get("/spells/{id}")]
async fn get_by_id(id: web::Path<String>) -> HttpResponse {
let id = match id.parse::<i32>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
match web::block(move || QuerySpell::get_by_id(id)).await.unwrap() {
Ok(query_spell) => {
let id = query_spell.id;
let mut spell = Spell::from(query_spell);
spell.id = Some(id);
HttpResponse::Ok().json(Response {
data: spell,
metadata: None,
})
}
Err(err) => {
error!("{:?}", err.message);
ResponseError::error_response(&err)
}
}
}
#[post("/spells")]
async fn create(spell: web::Json<Spell>, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") {
Ok(_) => {}
Err(err) => return ResponseError::error_response(&err),
};
match InsertSpell::insert(spell.into_inner().into()) {
Ok(spell) => HttpResponse::Created().json(Spell::from(spell)),
Err(err) => {
error!("{:?}", err.message);
ResponseError::error_response(&err)
}
}
}
#[put("/spells/{id}")]
async fn update(id: web::Path<String>, spell: web::Json<Spell>, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") {
Ok(_) => {}
Err(err) => return ResponseError::error_response(&err),
};
let id = match id.parse::<i32>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
match web::block(move || InsertSpell::update(id, spell.into_inner().into()))
.await
.unwrap()
{
Ok(spell) => HttpResponse::Ok().json(Spell::from(spell)),
Err(err) => {
error!("{:?}", err.message);
ResponseError::error_response(&err)
}
}
}
#[delete("/spells/{id}")]
async fn delete(id: web::Path<String>, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") {
Ok(_) => {}
Err(err) => return ResponseError::error_response(&err),
};
let id = match id.parse::<i32>() {
Ok(id) => id,
Err(err) => {
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string(),
})
}
};
match web::block(move || QuerySpell::delete(id)).await.unwrap() {
Ok(spell) => HttpResponse::Ok().json(Spell::from(spell)),
Err(err) => {
error!("{:?}", err.message);
ResponseError::error_response(&err)
}
}
}
pub fn init_routes(config: &mut web::ServiceConfig) {
config.service(
web::scope("dnd")
.service(get_all)
.service(get_by_id)
.service(create)
.service(update),
);
}

View File

@@ -1,3 +0,0 @@
mod routes;
pub use routes::init_routes;

View File

@@ -1,142 +0,0 @@
use actix_multipart::Multipart;
use actix_web::{web, HttpResponse, post, delete, get, ResponseError};
use log::error;
use serenity::futures::StreamExt;
use siren::ServiceError;
use crate::{
auth::{Auth, InsertUser, QueryUser},
storage::{upload_file, get_file, delete_file},
};
#[post("/picture")]
async fn set_picture(mut payload: Multipart, auth: Auth) -> HttpResponse {
while let Some(item) = payload.next().await {
let mut bytes = web::BytesMut::new();
let mut field = match item {
Ok(field) => field,
Err(err) => return ResponseError::error_response(&err),
};
let content_type = field.content_disposition();
// Get file name and construct the file path
let file_name = match content_type.get_filename() {
Some(name) => {
// Verify extension is supported
match name.split(".").last() {
Some(ext) => match ext {
"png" | "jpg" | "jpeg" => name,
_ => {
return ResponseError::error_response(&ServiceError {
status: 400,
message: "File extension is not supported".to_string(),
})
}
},
None => {
return ResponseError::error_response(&ServiceError {
status: 400,
message: "Unknown file extension".to_string(),
})
}
}
}
None => {
return ResponseError::error_response(&ServiceError {
status: 400,
message: "File name is not provided".to_string(),
})
}
};
let path = format!("users/{}/{}", auth.user.email, file_name);
// Build the file and store it in minio
while let Some(chunk) = field.next().await {
let data = match chunk {
Ok(data) => data,
Err(err) => {
error!("Failed to get chunk: {}", err);
return ResponseError::error_response(&err);
}
};
bytes.extend_from_slice(&data);
}
match upload_file(&path, &bytes).await {
Ok(_) => {
match InsertUser::update_profile(&auth.user.email, Some(&path)) {
Ok(_) => {}
Err(err) => {
error!("Failed to update user profile: {}", err);
return ResponseError::error_response(&err);
}
};
}
Err(err) => {
error!("Failed to upload file: {}", err);
return ResponseError::error_response(&err);
}
}
}
return HttpResponse::Ok().finish();
}
#[get("/picture")]
async fn get_picture(auth: Auth) -> HttpResponse {
let user = match QueryUser::get_by_email(&auth.user.email) {
Ok(user) => user,
Err(err) => {
error!("Failed to get user: {}", err);
return ResponseError::error_response(&err);
}
};
if let Some(path) = user.profile_picture {
match get_file(&path).await {
Ok(bytes) => return HttpResponse::Ok().body(bytes),
Err(err) => {
error!("Failed to get file: {}", err);
return ResponseError::error_response(&err);
}
}
} else {
return HttpResponse::NotFound().finish();
}
}
#[delete("/picture")]
async fn delete_picture(auth: Auth) -> HttpResponse {
match QueryUser::get_by_email(&auth.user.email) {
Ok(user) => match user.profile_picture {
Some(path) => {
match delete_file(&path).await {
Ok(_) => {
match InsertUser::update_profile(&auth.user.email, None) {
Ok(_) => {}
Err(err) => {
error!("Failed to update user profile: {}", err);
return ResponseError::error_response(&err);
}
};
}
Err(err) => {
error!("Failed to delete file: {}", err);
return ResponseError::error_response(&err);
}
};
}
None => {}
},
Err(err) => {
error!("Failed to get user: {}", err);
return ResponseError::error_response(&err);
}
};
return HttpResponse::Ok().finish();
}
pub fn init_routes(config: &mut web::ServiceConfig) {
config.service(
web::scope("users")
.service(set_picture)
.service(get_picture)
.service(delete_picture),
);
}

View File

@@ -9,9 +9,9 @@ use serenity::model::application::interaction::application_command::ApplicationC
use siren::ServiceError; use siren::ServiceError;
use songbird::{EventHandler, Songbird}; use songbird::{EventHandler, Songbird};
use crate::bot::guilds::QueryGuild;
use crate::bot::ytdlp::PlaylistItem; use crate::bot::ytdlp::PlaylistItem;
use crate::bot::{ use crate::bot::{
guilds::QueryGuild,
commands::audio::{leave, get_playlist_urls, add_song, get_songbird}, commands::audio::{leave, get_playlist_urls, add_song, get_songbird},
}; };

View File

@@ -5,8 +5,6 @@ use serenity::model::gateway::Ready;
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity::prelude::*; use serenity::prelude::*;
use crate::bot::guilds::InsertGuild;
use super::{commands, oai}; use super::{commands, oai};
use super::commands::audio::create_response; use super::commands::audio::create_response;
@@ -78,11 +76,6 @@ impl EventHandler for Handler {
warn!("No ready guilds found"); warn!("No ready guilds found");
} }
for guild in ready.guilds { for guild in ready.guilds {
let _ = InsertGuild::insert(InsertGuild {
id: (guild.id.0 as i64),
bot_id: ctx.cache.current_user().id.0 as i64,
volume: 100,
});
let commands = guild let commands = guild
.id .id
.set_application_commands(&ctx.http, |commands| { .set_application_commands(&ctx.http, |commands| {

3
src/bot/oai/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
mod model;
pub use model::*;

3
src/dnd/classes/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
mod model;
pub use model::*;

View File

@@ -1,5 +1,4 @@
mod model; mod model;
mod routes;
mod types; mod types;
use std::{ use std::{
@@ -11,7 +10,6 @@ use std::{
use log::{warn, trace}; use log::{warn, trace};
pub use model::*; pub use model::*;
pub use types::*; pub use types::*;
pub use routes::init_routes;
pub fn load_data(data_dir_path: &str) { pub fn load_data(data_dir_path: &str) {
if Path::new(data_dir_path).exists() { if Path::new(data_dir_path).exists() {

View File

@@ -5,32 +5,23 @@ extern crate diesel_migrations;
use std::env; use std::env;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use log::{error, warn, info};
use serenity::client::Cache; use serenity::client::Cache;
use serenity::framework::StandardFramework; use serenity::framework::StandardFramework;
use serenity::http::Http; use serenity::http::Http;
use serenity::prelude::*; use serenity::prelude::*;
use songbird::{SerenityInit, Songbird}; use songbird::{SerenityInit, Songbird};
use actix_cors::Cors;
use actix_web::{HttpServer, App, web};
use crate::bot::handler::Handler; use crate::bot::handler::Handler;
mod auth;
mod bot; mod bot;
mod dnd; mod dnd;
mod storage; mod storage;
mod users;
#[actix_web::main] #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,siren=info")); env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,siren=info"));
storage::init().await; storage::init().await;
match env::var("DATA_DIR_PATH") {
Ok(data_dir_path) => dnd::load_data(&data_dir_path),
Err(err) => warn!("Unable to load initial database data: {}", err),
};
let token: String = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); let token: String = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
let intents: GatewayIntents = GatewayIntents::all(); let intents: GatewayIntents = GatewayIntents::all();
@@ -54,7 +45,7 @@ async fn main() -> std::io::Result<()> {
let handler = match env::var("OPENAI_API_KEY") { let handler = match env::var("OPENAI_API_KEY") {
Ok(token) => { Ok(token) => {
info!("Loaded OpenAI token"); log::info!("Loaded OpenAI token");
let default_model = env::var("OPENAI_API_MODEL").unwrap_or("gpt-3.5-turbo".to_string()); let default_model = env::var("OPENAI_API_MODEL").unwrap_or("gpt-3.5-turbo".to_string());
Handler { Handler {
oai: Some(bot::oai::OAI { oai: Some(bot::oai::OAI {
@@ -70,7 +61,7 @@ async fn main() -> std::io::Result<()> {
} }
} }
Err(err) => { Err(err) => {
warn!("Could not load OpenAI token: {}", err); log::warn!("Could not load OpenAI token: {}", err);
Handler { oai: None } Handler { oai: None }
} }
}; };
@@ -84,62 +75,13 @@ async fn main() -> std::io::Result<()> {
.await .await
.expect("Error creating client"); .expect("Error creating client");
let http = Arc::clone(&client.cache_and_http.http); let _shard_manager = Arc::clone(&client.shard_manager);
let cache = Arc::clone(&client.cache_and_http.cache);
let app_data = Arc::new(AppState { // Start listening for events by starting a single shard
http,
cache,
songbird: Arc::clone(&songbird),
});
let shard_manager = Arc::clone(&client.shard_manager);
tokio::spawn(async move {
tokio::signal::ctrl_c()
.await
.expect("Could not register ctrl+c handler");
shard_manager.lock().await.shutdown_all().await;
});
tokio::spawn(async move {
if let Err(why) = client.start_autosharded().await { if let Err(why) = client.start_autosharded().await {
error!("An error occurred while running the client: {:?}", why); log::error!("Client error: {why:?}");
} }
});
let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string());
let port = env::var("SERVICE_PORT").unwrap_or("5000".to_string());
let server = match HttpServer::new(move || {
let cors = Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header()
.supports_credentials()
.max_age(3600);
App::new()
.wrap(cors)
.app_data(web::Data::new(Arc::clone(&app_data)))
.configure(crate::auth::init_routes)
.configure(crate::users::init_routes)
.configure(crate::dnd::spells::init_routes)
.configure(crate::bot::guilds::init_routes)
.configure(crate::bot::messages::init_routes)
})
.bind(format!("{}:{}", host, port))
{
Ok(b) => {
info!("Binding server to {}:{}", host, port);
b
}
Err(err) => {
error!("Could not bind server: {}", err);
return Err(err);
}
};
server.run().await
} }
pub struct AppState { pub struct AppState {

View File

@@ -1,5 +0,0 @@
SERVICE_HOST=service
SERVICE_PORT=5000
UI_PORT=3000
NODE_ENV=development

View File

@@ -1,17 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint/eslint-plugin"
],
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"rules": {
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}

View File

@@ -1 +0,0 @@
18.17.1

View File

@@ -1,8 +0,0 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 120
}

View File

@@ -1,39 +0,0 @@
# Base
FROM node:18-alpine AS base
# Dependencies
FROM base as deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Dev
FROM base AS dev
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Builder
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Runner
FROM base AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV NEXT_TELEMETRY_DISABLED 1
CMD ["node", "server.js"]

View File

@@ -1,26 +0,0 @@
#!make
SHELL := /bin/bash
.PHONY: help build start stop lint
help: ## This info
@echo
@cat Makefile | grep -E '^[a-zA-Z\/_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
@echo
build: ## Install the dependencies and build
docker compose build
up: ## Start the dev instance
docker compose --profile frontend up -d
down: ## Stop the dev instance
docker compose --profile frontend down
lint: ## Run the linter
npm run lint
clean: ## Remove node modules
docker compose down && \
docker image rm siren-ui

View File

@@ -1,26 +0,0 @@
name: siren
services:
ui:
container_name: siren-ui
env_file:
- .env
environment:
- NODE_ENV=${NODE_ENV:-development}
ports:
- ${UI_PORT:-3000}:3000
build:
context: ./
target: dev
command: "npm run dev"
volumes:
- ./src:/app/src
- ./public:/app/public
- ./styles:/app/styles
networks:
- siren-frontend
restart: unless-stopped
profiles:
- frontend
networks:
siren-frontend: {}

5
ui/next-env.d.ts vendored
View File

@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -1,15 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
eslint: {
ignoreDuringBuilds: true
},
publicRuntimeConfig: {
// remove private variables from processEnv
processEnv: Object.fromEntries(Object.entries(process.env).filter(([key]) => key.includes('NEXT_PUBLIC_')))
},
output: 'standalone'
};
module.exports = nextConfig;

5834
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,45 +0,0 @@
{
"name": "siren-ui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@mantine/core": "^7.5.0",
"@mantine/form": "^7.5.0",
"@mantine/hooks": "^7.5.0",
"@mantine/modals": "^7.5.0",
"@mantine/notifications": "^7.5.0",
"@pixi/react": "^7.1.1",
"js-cookie": "^3.0.5",
"next": "^14.1.0",
"next-auth": "^4.24.5",
"pixi.js": "^7.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.0.1",
"recoil": "^0.7.7"
},
"devDependencies": {
"@types/js-cookie": "^3.0.6",
"@types/node": "20.11.10",
"@types/react": "18.2.48",
"@types/react-dom": "18.2.18",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"autoprefixer": "^10.4.17",
"eslint": "8.56.0",
"eslint-config-next": "14.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"postcss": "^8.4.33",
"postcss-import": "^16.0.0",
"postcss-preset-mantine": "^1.12.3",
"prettier": "^3.2.4",
"typescript": "5.3.3"
}
}

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