Stripped out ui/api
This commit is contained in:
@@ -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
|
||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"rust-analyzer.linkedProjects": [
|
"rust-analyzer.linkedProjects": [
|
||||||
"./service/Cargo.toml"
|
"./Cargo.toml"
|
||||||
],
|
],
|
||||||
"rust-analyzer.showUnlinkedFileNotification": false
|
"rust-analyzer.showUnlinkedFileNotification": false
|
||||||
}
|
}
|
||||||
@@ -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
82
docker-compose.yml
Normal 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:
|
||||||
@@ -1 +0,0 @@
|
|||||||
SIREN_VERSION=0.2.6
|
|
||||||
@@ -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:
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod routes;
|
|
||||||
mod session;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use session::*;
|
|
||||||
pub use routes::init_routes;
|
|
||||||
@@ -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(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod routes;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use routes::init_routes;
|
|
||||||
@@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod routes;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use routes::init_routes;
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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 ¶ms.schools {
|
|
||||||
Some(schools) => Some(schools.split(",").map(|s| s.to_string()).collect()),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
filters.by_levels = match ¶ms.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 ¶ms.classes {
|
|
||||||
Some(classes) => Some(classes.split(",").map(|s| s.to_string()).collect()),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
filters.by_damage_inflict = match ¶ms.damage_inflict {
|
|
||||||
Some(damage_inflict) => Some(damage_inflict.split(",").map(|s| s.to_string()).collect()),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
filters.by_damage_resist = match ¶ms.damage_resist {
|
|
||||||
Some(damage_resist) => Some(damage_resist.split(",").map(|s| s.to_string()).collect()),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
filters.by_conditions = match ¶ms.conditions {
|
|
||||||
Some(conditions) => Some(conditions.split(",").map(|s| s.to_string()).collect()),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
filters.by_saving_throw = match ¶ms.saving_throw {
|
|
||||||
Some(saving_throw) => Some(saving_throw.split(",").map(|s| s.to_string()).collect()),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
filters.by_attack_type = match ¶ms.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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
mod routes;
|
|
||||||
|
|
||||||
pub use routes::init_routes;
|
|
||||||
@@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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
3
src/bot/oai/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod model;
|
||||||
|
|
||||||
|
pub use model::*;
|
||||||
3
src/dnd/classes/mod.rs
Normal file
3
src/dnd/classes/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod model;
|
||||||
|
|
||||||
|
pub use model::*;
|
||||||
@@ -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() {
|
||||||
@@ -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 {
|
||||||
5
ui/.env
5
ui/.env
@@ -1,5 +0,0 @@
|
|||||||
SERVICE_HOST=service
|
|
||||||
SERVICE_PORT=5000
|
|
||||||
|
|
||||||
UI_PORT=3000
|
|
||||||
NODE_ENV=development
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"trailingComma": "none",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
||||||
@@ -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"]
|
|
||||||
26
ui/Makefile
26
ui/Makefile
@@ -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
|
|
||||||
@@ -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
5
ui/next-env.d.ts
vendored
@@ -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.
|
|
||||||
@@ -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
5834
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
Reference in New Issue
Block a user