Compare commits

..

2 Commits
main ... build

Author SHA1 Message Date
Mohamad
12bf65f865 lol
Some checks failed
Build and Push Docker Image / build_and_push (push) Failing after 28s
2025-01-03 13:52:48 +01:00
Mohamad
5eb085067d docker sht
All checks were successful
Build and Push Docker Image / build_and_push (push) Successful in 1m7s
2025-01-03 13:45:35 +01:00
11 changed files with 26 additions and 530 deletions

View File

@ -5,16 +5,20 @@ COPY frontend/ .
RUN npm install
RUN npm run build
# Stage 2: Build the Rust backend
# Stage 2: Build the Rust backend (statically linked)
FROM rust:1.83 as backend-builder
WORKDIR /app/backend
COPY backend/ .
RUN cargo build --release
# Add the musl target for static linking
RUN rustup target add x86_64-unknown-linux-musl
# Build the binary with musl
RUN cargo build --release --target x86_64-unknown-linux-musl
# Final Stage: Combine frontend and backend
# Final Stage
FROM debian:bullseye-slim
WORKDIR /app
COPY --from=frontend-builder /app/frontend/build ./frontend/dist
COPY --from=backend-builder /app/backend/target/release/formies_be ./formies_be
COPY --from=frontend-builder /app/frontend/build ./frontend/build
# Copy the statically linked binary
COPY --from=backend-builder /app/backend/target/x86_64-unknown-linux-musl/release/formies_be ./formies_be
EXPOSE 8080
CMD ["./backend"]
CMD ["./formies_be"]

212
backend/Cargo.lock generated
View File

@ -338,15 +338,6 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -387,12 +378,6 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "byteorder"
version = "1.5.0"
@ -516,7 +501,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@ -607,7 +591,6 @@ dependencies = [
"env_logger",
"futures",
"log",
"rand_core",
"rusqlite",
"serde",
"serde_json",
@ -720,10 +703,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@ -1007,31 +988,6 @@ dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "jsonwebtoken"
version = "9.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f"
dependencies = [
"base64 0.21.7",
"js-sys",
"pem",
"ring",
"serde",
"serde_json",
"simple_asn1",
]
[[package]]
name = "language-tags"
version = "0.3.2"
@ -1137,40 +1093,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.36.7"
@ -1209,33 +1137,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pem"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
dependencies = [
"base64 0.22.1",
"serde",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -1367,21 +1274,6 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "ring"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"libc",
"spin",
"untrusted",
"windows-sys",
]
[[package]]
name = "rusqlite"
version = "0.29.0"
@ -1499,18 +1391,6 @@ dependencies = [
"libc",
]
[[package]]
name = "simple_asn1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
dependencies = [
"num-bigint",
"num-traits",
"thiserror",
"time",
]
[[package]]
name = "slab"
version = "0.4.9"
@ -1536,24 +1416,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.92"
@ -1585,26 +1453,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.37"
@ -1714,12 +1562,6 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.4"
@ -1776,60 +1618,6 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
[[package]]
name = "winapi-util"
version = "0.1.9"

View File

@ -6,7 +6,6 @@ use uuid::Uuid; // UUID for generating unique IDs // Import anyhow
pub fn init_db() -> AnyhowResult<Connection> {
let conn = Connection::open("form_data.db").context("Failed to open the database")?;
// Create tables
conn.execute(
"CREATE TABLE IF NOT EXISTS forms (
id TEXT PRIMARY KEY,

View File

@ -1,10 +1,4 @@
use crate::models::{AdminUser, Claims, Form, LoginCredentials, Submission};
use actix_web::{web, HttpResponse, Responder};
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use jsonwebtoken::{encode, EncodingKey, Header};
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
@ -140,101 +134,3 @@ pub async fn get_submissions(
submissions_iter.filter_map(|s| s.ok()).collect();
HttpResponse::Ok().json(submissions)
}
pub async fn admin_login(
db: web::Data<Arc<Mutex<Connection>>>,
credentials: web::Json<LoginCredentials>,
) -> impl Responder {
let conn = match db.lock() {
Ok(conn) => conn,
Err(_) => return HttpResponse::InternalServerError().body("Database lock error"),
};
let mut stmt =
match conn.prepare("SELECT username, password_hash FROM admin_users WHERE username = ?1") {
Ok(stmt) => stmt,
Err(e) => {
return HttpResponse::InternalServerError().body(format!("Database error: {}", e))
}
};
let admin: Option<AdminUser> = match stmt.query_row([&credentials.username], |row| {
Ok(AdminUser {
username: row.get(0)?,
password_hash: row.get(1)?,
})
}) {
Ok(admin) => Some(admin),
Err(rusqlite::Error::QueryReturnedNoRows) => None, // No user found
Err(e) => return HttpResponse::InternalServerError().body(format!("Query error: {}", e)),
};
match admin {
Some(user) => {
let parsed_hash = match PasswordHash::new(&user.password_hash) {
Ok(hash) => hash,
Err(_) => {
return HttpResponse::InternalServerError()
.body("Invalid password hash format in database")
}
};
let argon2 = Argon2::default();
let is_valid = argon2
.verify_password(credentials.password.as_bytes(), &parsed_hash)
.is_ok();
if is_valid {
let expiration = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(duration) => duration.as_secs() as usize + 24 * 3600,
Err(_) => return HttpResponse::InternalServerError().body("System time error"),
};
let claims = Claims {
sub: user.username,
exp: expiration,
};
let token = match encode(
&Header::default(),
&claims,
&EncodingKey::from_secret("your-secret-key".as_ref()),
) {
Ok(token) => token,
Err(_) => {
return HttpResponse::InternalServerError().body("Token generation error")
}
};
HttpResponse::Ok().json(json!({ "token": token }))
} else {
HttpResponse::Unauthorized().json(json!({ "error": "Invalid credentials" }))
}
}
None => HttpResponse::Unauthorized().json(json!({ "error": "Invalid credentials" })),
}
}
pub async fn create_admin(
db: web::Data<Arc<Mutex<Connection>>>,
user: web::Json<LoginCredentials>,
) -> impl Responder {
let conn = db.lock().unwrap();
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(user.password.as_bytes(), &salt)
.unwrap()
.to_string();
match conn.execute(
"INSERT INTO admin_users (username, password_hash) VALUES (?1, ?2)",
params![user.username, password_hash],
) {
Ok(_) => HttpResponse::Ok().json(json!({
"message": "Admin user created successfully"
})),
Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)),
}
}

View File

@ -5,30 +5,9 @@ use std::sync::{Arc, Mutex};
mod auth;
mod db;
mod handlers; // Ensure handlers.rs exists
mod middleware; // Ensure middleware.rs exists // Ensure db.rs exists
mod handlers;
mod models;
use crate::middleware::AuthMiddleware;
use handlers::{admin_login, create_admin, create_form, get_forms, get_submissions, submit_form};
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("")
.route("/forms/{id}", web::post().to(submit_form))
.route("/admin/login", web::post().to(admin_login))
.route("/admin/create", web::post().to(create_admin)),
);
cfg.service(
web::scope("")
.wrap(AuthMiddleware)
.route("/forms", web::get().to(get_forms))
.route("/forms", web::post().to(create_form))
.route("/forms/{id}/submissions", web::get().to(get_submissions)),
);
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
@ -45,7 +24,7 @@ async fn main() -> std::io::Result<()> {
.allow_any_method(),
)
.app_data(web::Data::new(db.clone()))
.service(fs::Files::new("/", "./frontend/dist").index_file("index.html"))
.service(fs::Files::new("/", "frontend/build").index_file("index.html"))
.route("/login", web::post().to(handlers::login)) // Public: Login
.route(
"/forms/{id}/submissions",
@ -58,7 +37,7 @@ async fn main() -> std::io::Result<()> {
web::get().to(handlers::get_submissions), // Protected
)
})
.bind("127.0.0.1:8080")?
.bind("0.0.0.0:8080")?
.run()
.await
}

View File

@ -1,87 +0,0 @@
use crate::models::Claims;
use actix_web::body::{BoxBody, MessageBody};
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::{Error, HttpResponse};
use futures::future::{ok, Ready};
use serde_json::json;
use std::future::Future;
use std::pin::Pin;
pub struct AuthMiddleware;
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<BoxBody>; // Changed to BoxBody
type Error = Error;
type Transform = AuthMiddlewareService<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(AuthMiddlewareService { service })
}
}
pub struct AuthMiddlewareService<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for AuthMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<BoxBody>; // Changed to BoxBody
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
if req.path() == "/admin/login" || req.path() == "/admin/create" {
let fut = self.service.call(req);
return Box::pin(async move {
let res = fut.await?;
Ok(res.map_into_boxed_body()) // Convert the response body to BoxBody
});
}
let auth_header = req.headers().get("Authorization");
match auth_header {
Some(header) => {
let token = header.to_str().unwrap_or("").replace("Bearer ", "");
if verify_token(&token) {
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
Ok(res.map_into_boxed_body()) // Convert the response body to BoxBody
})
} else {
let (request, _) = req.into_parts();
let response = HttpResponse::Unauthorized()
.json(json!({"error": "Invalid token"}))
.map_into_boxed_body();
Box::pin(async move { Ok(ServiceResponse::new(request, response)) })
}
}
None => {
let (request, _) = req.into_parts();
let response = HttpResponse::Unauthorized()
.json(json!({"error": "No authorization token"}))
.map_into_boxed_body();
Box::pin(async move { Ok(ServiceResponse::new(request, response)) })
}
}
}
}
pub fn verify_token(token: &str) -> bool {
let validation = jsonwebtoken::Validation::default();
let key = jsonwebtoken::DecodingKey::from_secret("your-secret-key".as_ref());
jsonwebtoken::decode::<Claims>(token, &key, &validation).is_ok()
}

View File

@ -13,21 +13,3 @@ pub struct Submission {
pub form_id: String,
pub data: serde_json::Value, // JSON of submission data
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminUser {
pub username: String,
pub password_hash: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginCredentials {
pub username: String,
pub password: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub(crate) exp: usize,
}

View File

@ -15,52 +15,8 @@ function delAuthToken(): void {
localStorage.removeItem('auth_token');
}
// A simple function to retrieve the token from local storage or wherever it is stored
function getAuthToken(): string | null {
return localStorage.getItem('auth_token'); // Assuming the token is stored in localStorage
}
// A simple function to save the token
function setAuthToken(token: string): void {
localStorage.setItem('auth_token', token);
}
// A simple function to save the token
function delAuthToken(): void {
localStorage.removeItem('auth_token');
}
/**
* Helper to make authenticated requests.
* @param endpoint The API endpoint (relative to base URL).
* @param options Fetch options such as method, headers, and body.
* @returns The JSON-parsed response.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function authenticatedRequest(endpoint: string, options: RequestInit): Promise<any> {
const token = localStorage.getItem('authToken'); // Replace with a secure token storage solution if needed
if (!token) {
throw new Error('Authentication token is missing. Please log in.');
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`, // Include the token in the Authorization header
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}
return response.json();
}
/**
* Create a new form (authenticated).
* Create a new form.
* @param name The name of the form.
* @param fields The fields of the form in JSON format.
* @returns The ID of the created form.
@ -75,10 +31,16 @@ export async function createForm(name: string, fields: unknown): Promise<string>
},
body: JSON.stringify({ name, fields })
});
if (!response.ok) {
throw new Error(`Error creating form: ${response.statusText}`);
}
return await response.json();
}
/**
* Get all forms (authenticated).
* Get all forms.
* @returns An array of forms.
*/
export async function getForms(): Promise<unknown[]> {
@ -99,7 +61,7 @@ export async function getForms(): Promise<unknown[]> {
}
/**
* Submit a form (unauthenticated).
* Submit a form.
* @param formId The ID of the form to submit.
* @param data The submission data in JSON format.
* @returns The ID of the created submission.
@ -123,9 +85,9 @@ export async function submitForm(formId: string, data: unknown): Promise<string>
}
/**
* Admin login to get a token.
* @param credentials The login credentials (username and password).
* @returns The generated JWT token if successful.
* Get all submissions for a specific form.
* @param formId The ID of the form.
* @returns An array of submissions for the form.
*/
export async function getSubmissions(formId: string): Promise<unknown[]> {
const token = getAuthToken(); // Get the token from storage

View File

@ -1,17 +0,0 @@
import type { AdminUser } from './types';
const key2 = 'username';
function login(user: AdminUser) {
localStorage.setItem(key2, user.username);
}
function logout() {
localStorage.removeItem(key2);
}
function loggedIn() {
return localStorage.getItem('authToken') !== null;
}
export default { login, logout, loggedIn };

View File

@ -17,13 +17,3 @@ export interface Submission {
data: Record<string, unknown>;
created_at?: string;
}
export interface LoginCredentials {
username: string;
password: string;
}
export interface AdminUser {
username: string;
password_hash: string;
}

View File

@ -3,7 +3,7 @@
import { getForms } from '../../../lib/api';
import type { Form } from '../../../lib/types';
let forms: Form[] = [];
let forms: any;
onMount(async () => {
forms = await getForms();