Compare commits
10 Commits
a538882628
...
6889377f07
Author | SHA1 | Date | |
---|---|---|---|
6889377f07 | |||
44f0b33fe5 | |||
fb2dd90181 | |||
26e666bae0 | |||
aa6a48a09b | |||
9f63255599 | |||
dbb1dd452e | |||
bac64f545f | |||
c884eae54c | |||
7a33ada21e |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
||||
/target
|
||||
database.db
|
||||
.env
|
||||
/scripts
|
||||
|
1254
Cargo.lock
generated
1254
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@ -5,5 +5,15 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
argon2 = { version = "0.5.3", features = ["password-hash", "rand"] }
|
||||
axum = "0.7.5"
|
||||
chrono = "0.4.38"
|
||||
dotenvy = "0.15.7"
|
||||
http = "1.1.0"
|
||||
jsonwebtoken = "9.3.0"
|
||||
rand = "0.8.5"
|
||||
rusqlite = "0.32.1"
|
||||
serde = { version = "1.0.206", features = ["derive"] }
|
||||
serde_json = "1.0.122"
|
||||
tokio = { version = "1.39.2", features = ["full"] }
|
||||
tower-http = { version = "0.5.2", features = ["fs"] }
|
||||
validator = { version = "0.18.1", features = ["derive"] }
|
||||
|
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@ -0,0 +1,23 @@
|
||||
FROM rust:1.80 AS builder
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
RUN mkdir src && echo "fn main() {}" > src/main.rs
|
||||
RUN cargo build --release
|
||||
|
||||
COPY src src
|
||||
RUN cargo install --path .
|
||||
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends sqlite3 libsqlite3-dev && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /usr/local/cargo/bin/script_hoster .
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["./script_hoster"]
|
||||
|
12
docker-compose.yaml
Normal file
12
docker-compose.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
services:
|
||||
web:
|
||||
restart: unless-stopped
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./.env:/app/.env:ro
|
||||
- data:/app
|
||||
|
||||
volumes:
|
||||
data:
|
463
src/api/mod.rs
Normal file
463
src/api/mod.rs
Normal file
@ -0,0 +1,463 @@
|
||||
use axum::{routing::post, Json, Router};
|
||||
use jsonwebtoken::{encode, DecodingKey, EncodingKey, Header};
|
||||
use rusqlite::Connection;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::db;
|
||||
|
||||
pub fn get_routes() -> Router {
|
||||
Router::new().nest(
|
||||
"/api",
|
||||
Router::new()
|
||||
.route("/register", post(register))
|
||||
.route("/login", post(login))
|
||||
.route("/change_username", post(change_username))
|
||||
.route("/change_email", post(change_email))
|
||||
.route("/change_password", post(change_password))
|
||||
.route("/delete_account", post(delete_account))
|
||||
.route("/get_scripts", post(get_scripts))
|
||||
.route("/add_script", post(add_script))
|
||||
.route("/delete_script", post(delete_script))
|
||||
.route("/edit_script", post(edit_script))
|
||||
.route("/rename_script", post(rename_script)),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
account_id: usize,
|
||||
exp: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
pub struct RegisterPayload {
|
||||
username: String,
|
||||
#[validate(email)]
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
pub async fn register(Json(payload): Json<RegisterPayload>) -> Json<Value> {
|
||||
if payload.password.len() < 8 {
|
||||
Json(json!({ "error": "Password is too short" }))
|
||||
} else if payload.password.len() > 64 {
|
||||
Json(json!({ "error": "Password is too long" }))
|
||||
} else if payload.username.len() < 3 {
|
||||
Json(json!({ "error": "Username is too short" }))
|
||||
} else if payload.username.len() > 32 {
|
||||
Json(json!({ "error": "Username is too long" }))
|
||||
} else if payload.validate().is_err() {
|
||||
Json(json!({ "error": "Invalid email" }))
|
||||
} else {
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
let result = db::add_account(
|
||||
&connection,
|
||||
&payload.username,
|
||||
&payload.email,
|
||||
&payload.password,
|
||||
);
|
||||
connection.close().expect("Failed to close database");
|
||||
|
||||
match result {
|
||||
Ok(_) => Json(json!({ "success": "User created with success" })),
|
||||
Err(rusqlite::Error::SqliteFailure(_, _)) => {
|
||||
Json(json!({ "error": "Username or email already taken" }))
|
||||
}
|
||||
Err(_) => Json(json!({ "error": "Failed to create user" })),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginPayload {
|
||||
username: Option<String>,
|
||||
email: Option<String>,
|
||||
password: String,
|
||||
}
|
||||
|
||||
pub async fn login(Json(payload): Json<LoginPayload>) -> Json<Value> {
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
let account_id = if let Some(username) = payload.username {
|
||||
db::get_account_id_from_username(&connection, &username)
|
||||
} else if let Some(email) = payload.email {
|
||||
db::get_account_id_from_email(&connection, &email)
|
||||
} else {
|
||||
connection.close().expect("Failed to close database");
|
||||
return Json(json!({ "error": "Username or email must be provided" }));
|
||||
};
|
||||
|
||||
let account_id = match account_id {
|
||||
Ok(Some(account_id)) => account_id,
|
||||
Ok(None) => {
|
||||
connection.close().expect("Failed to close database");
|
||||
return Json(json!({ "error": "User not found" }));
|
||||
}
|
||||
Err(_) => {
|
||||
connection.close().expect("Failed to close database");
|
||||
return Json(json!({ "error": "Failed to get user" }));
|
||||
}
|
||||
};
|
||||
|
||||
let account = db::get_account(&connection, account_id, &payload.password);
|
||||
match account {
|
||||
Ok(Some(_)) => {
|
||||
connection.close().expect("Failed to close database");
|
||||
|
||||
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||
let claims = Claims {
|
||||
account_id,
|
||||
exp: (chrono::Utc::now() + chrono::Duration::days(7)).timestamp() as usize,
|
||||
};
|
||||
let token = encode(
|
||||
&Header::default(),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(jwt_secret.as_ref()),
|
||||
)
|
||||
.expect("Failed to encode token");
|
||||
|
||||
Json(json!({ "success": "User logged in with success", "token": token}))
|
||||
}
|
||||
Ok(None) => {
|
||||
connection.close().expect("Failed to close database");
|
||||
Json(json!({ "error": "Wrong password" }))
|
||||
}
|
||||
Err(_) => {
|
||||
connection.close().expect("Failed to close database");
|
||||
Json(json!({ "error": "Failed to get user" }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChangeUsernamePayload {
|
||||
username: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
fn get_account_id_from_jwt(token: &str) -> Result<usize, jsonwebtoken::errors::Error> {
|
||||
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||
let token_data = jsonwebtoken::decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(jwt_secret.as_ref()),
|
||||
&jsonwebtoken::Validation::default(),
|
||||
)?;
|
||||
Ok(token_data.claims.account_id)
|
||||
}
|
||||
|
||||
pub async fn change_username(Json(payload): Json<ChangeUsernamePayload>) -> Json<Value> {
|
||||
let account_id = match get_account_id_from_jwt(&payload.token) {
|
||||
Ok(account_id) => account_id,
|
||||
Err(_) => return Json(json!({ "error": "Invalid token" })),
|
||||
};
|
||||
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
match db::change_username(&connection, account_id, &payload.username) {
|
||||
Ok(_) => {}
|
||||
Err(rusqlite::Error::SqliteFailure(_, _)) => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Username already taken" }));
|
||||
}
|
||||
Err(_) => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Failed to change username" }));
|
||||
}
|
||||
}
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "success": "Username changed with success" }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChangeEmailPayload {
|
||||
email: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn change_email(Json(payload): Json<ChangeEmailPayload>) -> Json<Value> {
|
||||
let account_id = match get_account_id_from_jwt(&payload.token) {
|
||||
Ok(account_id) => account_id,
|
||||
Err(_) => return Json(json!({ "error": "Invalid token" })),
|
||||
};
|
||||
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
match db::change_email(&connection, account_id, &payload.email) {
|
||||
Ok(_) => {}
|
||||
Err(rusqlite::Error::SqliteFailure(_, _)) => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Email already taken" }));
|
||||
}
|
||||
Err(_) => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Failed to change email" }));
|
||||
}
|
||||
}
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "success": "Email changed with success" }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChangePasswordPayload {
|
||||
password: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn change_password(Json(payload): Json<ChangePasswordPayload>) -> Json<Value> {
|
||||
let account_id = match get_account_id_from_jwt(&payload.token) {
|
||||
Ok(account_id) => account_id,
|
||||
Err(_) => return Json(json!({ "error": "Invalid token" })),
|
||||
};
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
match db::change_password(&connection, account_id, &payload.password) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Failed to change password" }));
|
||||
}
|
||||
}
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "success": "Password changed with success" }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GenericTokenPayload {
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn delete_account(Json(payload): Json<GenericTokenPayload>) -> Json<Value> {
|
||||
let account_id = match get_account_id_from_jwt(&payload.token) {
|
||||
Ok(account_id) => account_id,
|
||||
Err(_) => return Json(json!({ "error": "Invalid token" })),
|
||||
};
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
match db::delete_account(&connection, account_id) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Failed to delete account" }));
|
||||
}
|
||||
}
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "success": "Account deleted with success" }))
|
||||
}
|
||||
|
||||
pub async fn get_scripts(Json(payload): Json<GenericTokenPayload>) -> Json<Value> {
|
||||
let account_id = match get_account_id_from_jwt(&payload.token) {
|
||||
Ok(account_id) => account_id,
|
||||
Err(_) => return Json(json!({ "error": "Invalid token" })),
|
||||
};
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
let scripts = db::get_scripts(&connection, account_id);
|
||||
connection.close().expect("Failed to close database");
|
||||
match scripts {
|
||||
Ok(scripts) => {
|
||||
let mut result = json!({});
|
||||
for (id, name, last_edit) in scripts {
|
||||
result[id.to_string()] = json!({ "name": name, "last_edit": last_edit });
|
||||
}
|
||||
Json(result)
|
||||
}
|
||||
Err(_) => Json(json!({ "error": "Failed to get scripts" })),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AddScriptPayload {
|
||||
name: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn add_script(Json(payload): Json<AddScriptPayload>) -> Json<Value> {
|
||||
let account_id = match get_account_id_from_jwt(&payload.token) {
|
||||
Ok(account_id) => account_id,
|
||||
Err(_) => return Json(json!({ "error": "Invalid token" })),
|
||||
};
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
match db::add_script(&connection, &payload.name, account_id) {
|
||||
Ok(_) => {
|
||||
let username = match db::get_account_username(&connection, account_id)
|
||||
.expect("Failed to get account username")
|
||||
{
|
||||
Some(username) => username,
|
||||
None => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Failed to get account username" }));
|
||||
}
|
||||
};
|
||||
let filename = format!("scripts/{}/{}", username, &payload.name);
|
||||
std::fs::create_dir_all(format!("scripts/{}", username))
|
||||
.expect("Failed to create directory");
|
||||
File::create(&filename).expect("Failed to create file");
|
||||
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "success": "Script added with success" }))
|
||||
}
|
||||
Err(_) => {
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "error": "Failed to add script" }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteScriptPayload {
|
||||
script_id: usize,
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn delete_script(Json(payload): Json<DeleteScriptPayload>) -> Json<Value> {
|
||||
let account_id = match get_account_id_from_jwt(&payload.token) {
|
||||
Ok(account_id) => account_id,
|
||||
Err(_) => return Json(json!({ "error": "Invalid token" })),
|
||||
};
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
if !db::script_is_owned_by(&connection, payload.script_id, account_id)
|
||||
.expect("Failed to check if script is owned by user")
|
||||
{
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Script is not owned by user" }));
|
||||
}
|
||||
let script_name = match db::get_script_name(&connection, payload.script_id, account_id)
|
||||
.expect("Failed to get script name")
|
||||
{
|
||||
Some(script_name) => script_name,
|
||||
None => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Failed to get script name" }));
|
||||
}
|
||||
};
|
||||
|
||||
match db::remove_script(&connection, payload.script_id) {
|
||||
Ok(_) => {
|
||||
let username = match db::get_account_username(&connection, account_id)
|
||||
.expect("Failed to get account username")
|
||||
{
|
||||
Some(username) => username,
|
||||
None => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Failed to get account username" }));
|
||||
}
|
||||
};
|
||||
let filename = format!("scripts/{}/{}", username, script_name);
|
||||
std::fs::remove_file(&filename).expect("Failed to remove file");
|
||||
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "success": "Script deleted with success" }))
|
||||
}
|
||||
Err(_) => {
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "error": "Failed to delete script" }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditScriptPayload {
|
||||
script_id: usize,
|
||||
content: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn edit_script(Json(payload): Json<EditScriptPayload>) -> Json<Value> {
|
||||
let account_id = match get_account_id_from_jwt(&payload.token) {
|
||||
Ok(account_id) => account_id,
|
||||
Err(_) => return Json(json!({ "error": "Invalid token" })),
|
||||
};
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
if !db::script_is_owned_by(&connection, payload.script_id, account_id)
|
||||
.expect("Failed to check if script is owned by user")
|
||||
{
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Script is not owned by user" }));
|
||||
}
|
||||
match db::update_script_last_edit(&connection, payload.script_id) {
|
||||
Ok(_) => {
|
||||
let username = match db::get_account_username(&connection, account_id)
|
||||
.expect("Failed to get account username")
|
||||
{
|
||||
Some(username) => username,
|
||||
None => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Failed to get account username" }));
|
||||
}
|
||||
};
|
||||
let script_name = match db::get_script_name(&connection, payload.script_id, account_id)
|
||||
.expect("Failed to get script name")
|
||||
{
|
||||
Some(script_name) => script_name,
|
||||
None => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Failed to get script name" }));
|
||||
}
|
||||
};
|
||||
let filename = format!("scripts/{}/{}", username, script_name);
|
||||
let mut file = File::create(&filename).expect("Failed to open file");
|
||||
file.write_all(payload.content.as_bytes())
|
||||
.expect("Failed to write to file");
|
||||
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "success": "Script edited with success" }))
|
||||
}
|
||||
Err(_) => {
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "error": "Failed to edit script" }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RenameScriptPayload {
|
||||
script_id: usize,
|
||||
new_script_name: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn rename_script(Json(payload): Json<RenameScriptPayload>) -> Json<Value> {
|
||||
let account_id = match get_account_id_from_jwt(&payload.token) {
|
||||
Ok(account_id) => account_id,
|
||||
Err(_) => return Json(json!({ "error": "Invalid token" })),
|
||||
};
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
if !db::script_is_owned_by(&connection, payload.script_id, account_id)
|
||||
.expect("Failed to check if script is owned by user")
|
||||
{
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Script is not owned by user" }));
|
||||
}
|
||||
let old_script_name = match db::get_script_name(&connection, payload.script_id, account_id)
|
||||
.expect("Failed to get script name")
|
||||
{
|
||||
Some(script_name) => script_name,
|
||||
None => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Failed to get script name" }));
|
||||
}
|
||||
};
|
||||
|
||||
match db::modify_script_name(&connection, payload.script_id, &payload.new_script_name) {
|
||||
Ok(_) => {
|
||||
let username = match db::get_account_username(&connection, account_id)
|
||||
.expect("Failed to get account username")
|
||||
{
|
||||
Some(username) => username,
|
||||
None => {
|
||||
connection.close().expect("Failed to close");
|
||||
return Json(json!({ "error": "Failed to get account username" }));
|
||||
}
|
||||
};
|
||||
let old_filename = format!("scripts/{}/{}", username, old_script_name);
|
||||
let new_filename = format!("scripts/{}/{}", username, &payload.new_script_name);
|
||||
std::fs::rename(&old_filename, &new_filename).expect("Failed to rename file");
|
||||
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "success": "Script renamed with success" }))
|
||||
}
|
||||
Err(_) => {
|
||||
connection.close().expect("Failed to close");
|
||||
Json(json!({ "error": "Failed to rename script" }))
|
||||
}
|
||||
}
|
||||
}
|
5
src/assets/style.css
Normal file
5
src/assets/style.css
Normal file
@ -0,0 +1,5 @@
|
||||
@import url('https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css');
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
@ -122,7 +122,17 @@ pub fn get_account_id_from_email(conn: &Connection, email: &str) -> Result<Optio
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_scripts(conn: &Connection, account_id: usize) -> Result<Vec<(usize, String, usize)>> {
|
||||
pub fn get_account_username(conn: &Connection, id: usize) -> Result<Option<String>> {
|
||||
let mut stmt = conn.prepare("SELECT username FROM accounts WHERE id = ?1")?;
|
||||
let mut rows = stmt.query([id])?;
|
||||
if let Some(row) = rows.next()? {
|
||||
Ok(Some(row.get(0)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_scripts(conn: &Connection, account_id: usize) -> Result<Vec<(usize, String, String)>> {
|
||||
let mut stmt = conn.prepare("SELECT id, name, last_edit FROM scripts WHERE account_id = ?1")?;
|
||||
let mut rows = stmt.query([account_id])?;
|
||||
let mut scripts = Vec::new();
|
||||
@ -138,12 +148,7 @@ pub fn get_scripts(conn: &Connection, account_id: usize) -> Result<Vec<(usize, S
|
||||
Ok(scripts)
|
||||
}
|
||||
|
||||
pub fn add_script(
|
||||
conn: &Connection,
|
||||
name: &str,
|
||||
last_edit: usize,
|
||||
account_id: usize,
|
||||
) -> Result<()> {
|
||||
pub fn add_script(conn: &Connection, name: &str, account_id: usize) -> Result<()> {
|
||||
let mut stmt = conn.prepare("SELECT id FROM scripts WHERE name = ?1 AND account_id = ?2")?;
|
||||
let mut rows = stmt.query([&name as &dyn ToSql, &account_id as &dyn ToSql])?;
|
||||
if rows.next()?.is_some() {
|
||||
@ -151,8 +156,8 @@ pub fn add_script(
|
||||
}
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO scripts (name, last_edit, account_id) VALUES (?1, ?2, ?3)",
|
||||
(name, last_edit, account_id),
|
||||
"INSERT INTO scripts (name, account_id) VALUES (?1, ?2)",
|
||||
(name, account_id),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
@ -169,10 +174,15 @@ pub fn script_is_owned_by(conn: &Connection, script_id: usize, account_id: usize
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_script_id(conn: &Connection, name: &str, account_id: usize) -> Result<Option<usize>> {
|
||||
let mut stmt = conn.prepare("SELECT id FROM scripts WHERE name = ?1 AND account_id = ?2")?;
|
||||
let mut rows = stmt.query([&name as &dyn ToSql, &account_id as &dyn ToSql])?;
|
||||
pub fn get_script_name(
|
||||
conn: &Connection,
|
||||
script_id: usize,
|
||||
account_id: usize,
|
||||
) -> Result<Option<String>> {
|
||||
let mut stmt = conn.prepare("SELECT name FROM scripts WHERE id = ?1 AND account_id = ?2")?;
|
||||
let mut rows = stmt.query([&script_id as &dyn ToSql, &account_id as &dyn ToSql])?;
|
||||
if let Some(row) = rows.next()? {
|
||||
println!("row: {:?}", row);
|
||||
Ok(Some(row.get(0)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
|
19
src/error_pages/404.html
Normal file
19
src/error_pages/404.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>404 Not found</title>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
</head>
|
||||
<body class="center">
|
||||
<header>
|
||||
<h1>404</h1>
|
||||
<h2>Page not found</h2>
|
||||
</header>
|
||||
<main>
|
||||
<p>Sorry, the page you are looking for does not exist.</p>
|
||||
<p>If you arrived here via a link, please open an issue on the repository.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
23
src/frontend/mod.rs
Normal file
23
src/frontend/mod.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use axum::handler::HandlerWithoutStateExt;
|
||||
use axum::response::{Html, IntoResponse};
|
||||
use axum::Router;
|
||||
use http::StatusCode;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub fn get_routes() -> Router {
|
||||
Router::new()
|
||||
.nest_service(
|
||||
"/",
|
||||
ServeDir::new("src/static_pages").not_found_service(handler.into_service()),
|
||||
)
|
||||
.nest_service("/assets", ServeDir::new("src/assets"))
|
||||
}
|
||||
|
||||
pub async fn handler() -> impl IntoResponse {
|
||||
let path = "src/error_pages/404.html";
|
||||
if let Ok(content) = tokio::fs::read_to_string(path).await {
|
||||
(StatusCode::NOT_FOUND, Html(content))
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, Html("404 Not Found".to_string()))
|
||||
}
|
||||
}
|
26
src/main.rs
26
src/main.rs
@ -1,8 +1,32 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use axum::Router;
|
||||
use rusqlite::Connection;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
mod api;
|
||||
mod db;
|
||||
mod frontend;
|
||||
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let connection = Connection::open("database.db").expect("Failed to open database");
|
||||
db::init(&connection).expect("Failed to create database");
|
||||
connection.close().expect("Failed to close database");
|
||||
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
let app = Router::new()
|
||||
.merge(api::get_routes())
|
||||
.merge(frontend::get_routes())
|
||||
.nest_service("/scripts", ServeDir::new("scripts"));
|
||||
|
||||
let port = std::env::var("PORT")
|
||||
.unwrap_or_else(|_| "3000".to_string())
|
||||
.parse()
|
||||
.expect("PORT must be an unsigned number");
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user