This commit is contained in:
commit
92c85ebe40
67
backend/Cargo.lock
generated
67
backend/Cargo.lock
generated
@ -282,16 +282,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
name = "anyhow"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures",
|
||||
"password-hash",
|
||||
]
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
@ -316,9 +310,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
@ -327,10 +321,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
name = "bcrypt"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
checksum = "a7e7c93a3fb23b2fdde989b2c9ec4dd153063ec81f408507f84c090cd91c6641"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"blowfish",
|
||||
"getrandom",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
@ -356,6 +356,16 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "6.0.0"
|
||||
@ -421,6 +431,16 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
@ -582,10 +602,10 @@ dependencies = [
|
||||
"actix-cors",
|
||||
"actix-files",
|
||||
"actix-web",
|
||||
"argon2",
|
||||
"anyhow",
|
||||
"bcrypt",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"jsonwebtoken",
|
||||
"log",
|
||||
"rand_core",
|
||||
"rusqlite",
|
||||
@ -952,6 +972,15 @@ dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.13"
|
||||
@ -1961,6 +1990,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.10.4"
|
||||
|
@ -11,9 +11,8 @@ serde_json = "1.0"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
actix-files = "0.6"
|
||||
actix-cors = "0.6"
|
||||
env_logger = "0.10" # Check for the latest version
|
||||
env_logger = "0.10"
|
||||
log = "0.4"
|
||||
jsonwebtoken = "9"
|
||||
argon2 = { version = "0.5", features = ["password-hash"] }
|
||||
futures = "0.3"
|
||||
rand_core = "0.6.4"
|
||||
bcrypt = "0.13"
|
||||
anyhow = "1.0"
|
Binary file not shown.
36
backend/src/auth.rs
Normal file
36
backend/src/auth.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use actix_web::{dev::Payload, http::header::AUTHORIZATION, web, Error, FromRequest, HttpRequest};
|
||||
use futures::future::{ready, Ready};
|
||||
use rusqlite::Connection;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct Auth {
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
impl FromRequest for Auth {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
let db = req
|
||||
.app_data::<web::Data<Arc<Mutex<Connection>>>>()
|
||||
.expect("Database connection missing");
|
||||
|
||||
if let Some(auth_header) = req.headers().get(AUTHORIZATION) {
|
||||
if let Ok(auth_str) = auth_header.to_str() {
|
||||
if auth_str.starts_with("Bearer ") {
|
||||
let token = &auth_str[7..];
|
||||
let conn = db.lock().unwrap();
|
||||
|
||||
match super::db::validate_token(&conn, token) {
|
||||
Ok(Some(user_id)) => return ready(Ok(Auth { user_id })),
|
||||
Ok(None) | Err(_) => {
|
||||
return ready(Err(actix_web::error::ErrorUnauthorized("Invalid token")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ready(Err(actix_web::error::ErrorUnauthorized("Missing token")))
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
use argon2::{password_hash::SaltString, Algorithm, Argon2, Params, PasswordHasher, Version};
|
||||
use rand_core::OsRng;
|
||||
use rusqlite::{Connection, Result};
|
||||
use anyhow::{Context, Result as AnyhowResult};
|
||||
use bcrypt::{hash, verify, DEFAULT_COST}; // Add bcrypt dependency for password hashing
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use uuid::Uuid; // UUID for generating unique IDs // Import anyhow
|
||||
|
||||
pub fn init_db() -> Result<Connection> {
|
||||
let conn = Connection::open("form_data.db")?;
|
||||
pub fn init_db() -> AnyhowResult<Connection> {
|
||||
let conn = Connection::open("form_data.db").context("Failed to open the database")?;
|
||||
|
||||
// Create tables
|
||||
conn.execute(
|
||||
@ -27,37 +28,96 @@ pub fn init_db() -> Result<Connection> {
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Add a table for users
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS admin_users (
|
||||
username TEXT PRIMARY KEY,
|
||||
password_hash TEXT NOT NULL
|
||||
"CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL, -- Store a hashed password
|
||||
token TEXT UNIQUE -- Optional: For token-based auth
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Check if the admin_users table is empty
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM admin_users", [], |row| row.get(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
if count == 0 {
|
||||
// Create a default admin user
|
||||
let default_username = "admin";
|
||||
let default_password = "admin123"; // This should be replaced with a secure method for real applications
|
||||
|
||||
// Hash the default password
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default());
|
||||
let password_hash = argon2
|
||||
.hash_password(default_password.as_bytes(), &salt)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO admin_users (username, password_hash) VALUES (?1, ?2)",
|
||||
&[default_username, password_hash.as_str()],
|
||||
)?;
|
||||
}
|
||||
// Setup initial admin after creating the tables
|
||||
setup_initial_admin(&conn)?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub fn setup_initial_admin(conn: &Connection) -> AnyhowResult<()> {
|
||||
add_admin_user(conn)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_admin_user(conn: &Connection) -> AnyhowResult<()> {
|
||||
// Check if admin user already exists
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id FROM users WHERE username = ?1")
|
||||
.context("Failed to prepare query for checking admin user")?;
|
||||
if stmt.exists(params!["admin"])? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Generate a UUID for the admin user
|
||||
let admin_id = Uuid::new_v4().to_string();
|
||||
|
||||
// Hash the password before storing it
|
||||
let hashed_password = hash("admin", DEFAULT_COST).context("Failed to hash password")?;
|
||||
|
||||
// Add admin user with hashed password
|
||||
conn.execute(
|
||||
"INSERT INTO users (id, username, password) VALUES (?1, ?2, ?3)",
|
||||
params![admin_id, "admin", hashed_password],
|
||||
)
|
||||
.context("Failed to insert admin user into the database")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Add a function to validate a token
|
||||
pub fn validate_token(conn: &Connection, token: &str) -> AnyhowResult<Option<String>> {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id FROM users WHERE token = ?1")
|
||||
.context("Failed to prepare query for validating token")?;
|
||||
let user_id: Option<String> = stmt
|
||||
.query_row(params![token], |row| row.get(0))
|
||||
.optional()
|
||||
.context("Failed to retrieve user ID for the given token")?;
|
||||
Ok(user_id)
|
||||
}
|
||||
|
||||
// Add a function to authenticate users (for login)
|
||||
pub fn authenticate_user(
|
||||
conn: &Connection,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> AnyhowResult<Option<String>> {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, password FROM users WHERE username = ?1")
|
||||
.context("Failed to prepare query for authenticating user")?;
|
||||
let mut rows = stmt
|
||||
.query(params![username])
|
||||
.context("Failed to execute query for authenticating user")?;
|
||||
|
||||
if let Some(row) = rows.next()? {
|
||||
let user_id: String = row.get(0)?;
|
||||
let stored_password: String = row.get(1)?;
|
||||
|
||||
// Use bcrypt to verify the hashed password
|
||||
if verify(password, &stored_password).context("Failed to verify password")? {
|
||||
return Ok(Some(user_id));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Add a function to generate and save a token for a user
|
||||
pub fn generate_token_for_user(conn: &Connection, user_id: &str, token: &str) -> AnyhowResult<()> {
|
||||
conn.execute(
|
||||
"UPDATE users SET token = ?1 WHERE id = ?2",
|
||||
params![token, user_id],
|
||||
)
|
||||
.context("Failed to update token for user")?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -6,20 +6,74 @@ use argon2::{
|
||||
};
|
||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||
use rusqlite::{params, Connection};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use uuid::Uuid;
|
||||
|
||||
// Create a new form
|
||||
use crate::auth::Auth;
|
||||
|
||||
// Structs for requests and responses
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LoginResponse {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
// Public: Login handler
|
||||
pub async fn login(
|
||||
db: web::Data<Arc<Mutex<Connection>>>,
|
||||
login_request: web::Json<LoginRequest>,
|
||||
) -> impl Responder {
|
||||
let conn = db.lock().unwrap();
|
||||
let user_id =
|
||||
match crate::db::authenticate_user(&conn, &login_request.username, &login_request.password)
|
||||
{
|
||||
Ok(Some(user_id)) => user_id,
|
||||
Ok(None) => return HttpResponse::Unauthorized().body("Invalid username or password"),
|
||||
Err(_) => return HttpResponse::InternalServerError().body("Database error"),
|
||||
};
|
||||
|
||||
let token = Uuid::new_v4().to_string();
|
||||
if let Err(_) = crate::db::generate_token_for_user(&conn, &user_id, &token) {
|
||||
return HttpResponse::InternalServerError().body("Failed to generate token");
|
||||
}
|
||||
|
||||
HttpResponse::Ok().json(LoginResponse { token })
|
||||
}
|
||||
|
||||
// Public: Submit a form
|
||||
pub async fn submit_form(
|
||||
db: web::Data<Arc<Mutex<Connection>>>,
|
||||
path: web::Path<String>,
|
||||
submission: web::Form<serde_json::Value>,
|
||||
) -> impl Responder {
|
||||
let conn = db.lock().unwrap();
|
||||
let submission_id = Uuid::new_v4().to_string();
|
||||
let form_id = path.into_inner();
|
||||
let submission_json = serde_json::to_string(&submission.into_inner()).unwrap();
|
||||
|
||||
match conn.execute(
|
||||
"INSERT INTO submissions (id, form_id, data) VALUES (?1, ?2, ?3)",
|
||||
params![submission_id, form_id, submission_json],
|
||||
) {
|
||||
Ok(_) => HttpResponse::Ok().json(submission_id),
|
||||
Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// Protected: Create a new form
|
||||
pub async fn create_form(
|
||||
db: web::Data<Arc<Mutex<Connection>>>,
|
||||
form: web::Json<Form>,
|
||||
auth: Auth,
|
||||
form: web::Json<crate::models::Form>,
|
||||
) -> impl Responder {
|
||||
println!("Received form: {:?}", form);
|
||||
let conn = db.lock().unwrap(); // Lock the Mutex to access the database
|
||||
println!("Authenticated user: {}", auth.user_id);
|
||||
let conn = db.lock().unwrap();
|
||||
let form_id = Uuid::new_v4().to_string();
|
||||
let form_json = serde_json::to_string(&form.fields).unwrap();
|
||||
|
||||
@ -32,9 +86,10 @@ pub async fn create_form(
|
||||
}
|
||||
}
|
||||
|
||||
// Get all forms
|
||||
pub async fn get_forms(db: web::Data<Arc<Mutex<Connection>>>) -> impl Responder {
|
||||
let conn = db.lock().unwrap(); // Lock the Mutex to access the database
|
||||
// Protected: Get all forms
|
||||
pub async fn get_forms(db: web::Data<Arc<Mutex<Connection>>>, auth: Auth) -> impl Responder {
|
||||
println!("Authenticated user: {}", auth.user_id);
|
||||
let conn = db.lock().unwrap();
|
||||
|
||||
let mut stmt = match conn.prepare("SELECT id, name, fields FROM forms") {
|
||||
Ok(stmt) => stmt,
|
||||
@ -47,41 +102,22 @@ pub async fn get_forms(db: web::Data<Arc<Mutex<Connection>>>) -> impl Responder
|
||||
let name: String = row.get(1)?;
|
||||
let fields: String = row.get(2)?;
|
||||
let fields = serde_json::from_str(&fields).unwrap();
|
||||
Ok(Form { id, name, fields })
|
||||
Ok(crate::models::Form { id, name, fields })
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let forms: Vec<Form> = forms_iter.filter_map(|f| f.ok()).collect();
|
||||
let forms: Vec<crate::models::Form> = forms_iter.filter_map(|f| f.ok()).collect();
|
||||
HttpResponse::Ok().json(forms)
|
||||
}
|
||||
|
||||
// Submit a form
|
||||
pub async fn submit_form(
|
||||
db: web::Data<Arc<Mutex<rusqlite::Connection>>>,
|
||||
path: web::Path<String>,
|
||||
submission: web::Form<serde_json::Value>,
|
||||
) -> impl Responder {
|
||||
let conn = db.lock().unwrap(); // Lock the Mutex to access the database
|
||||
let submission_id = Uuid::new_v4().to_string();
|
||||
let form_id = path.into_inner();
|
||||
|
||||
let submission_json = serde_json::to_string(&submission.into_inner()).unwrap();
|
||||
|
||||
match conn.execute(
|
||||
"INSERT INTO submissions (id, form_id, data) VALUES (?1, ?2, ?3)",
|
||||
params![submission_id, form_id, submission_json],
|
||||
) {
|
||||
Ok(_) => HttpResponse::Ok().json(submission_id),
|
||||
Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// Get submissions for a form
|
||||
// Protected: Get submissions for a form
|
||||
pub async fn get_submissions(
|
||||
db: web::Data<Arc<Mutex<Connection>>>,
|
||||
auth: Auth,
|
||||
path: web::Path<String>,
|
||||
) -> impl Responder {
|
||||
let conn = db.lock().unwrap(); // Lock the Mutex to access the database
|
||||
println!("Authenticated user: {}", auth.user_id);
|
||||
let conn = db.lock().unwrap();
|
||||
let form_id = path.into_inner();
|
||||
|
||||
let mut stmt =
|
||||
@ -96,11 +132,12 @@ pub async fn get_submissions(
|
||||
let form_id: String = row.get(1)?;
|
||||
let data: String = row.get(2)?;
|
||||
let data = serde_json::from_str(&data).unwrap();
|
||||
Ok(Submission { id, form_id, data })
|
||||
Ok(crate::models::Submission { id, form_id, data })
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let submissions: Vec<Submission> = submissions_iter.filter_map(|s| s.ok()).collect();
|
||||
let submissions: Vec<crate::models::Submission> =
|
||||
submissions_iter.filter_map(|s| s.ok()).collect();
|
||||
HttpResponse::Ok().json(submissions)
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ use actix_cors::Cors;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
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
|
||||
@ -43,7 +44,17 @@ async fn main() -> std::io::Result<()> {
|
||||
.allow_any_method(),
|
||||
)
|
||||
.app_data(web::Data::new(db.clone()))
|
||||
.configure(configure_routes)
|
||||
.route("/login", web::post().to(handlers::login)) // Public: Login
|
||||
.route(
|
||||
"/forms/{id}/submissions",
|
||||
web::post().to(handlers::submit_form), // Public: Submit form
|
||||
)
|
||||
.route("/forms", web::post().to(handlers::create_form)) // Protected
|
||||
.route("/forms", web::get().to(handlers::get_forms)) // Protected
|
||||
.route(
|
||||
"/forms/{id}/submissions",
|
||||
web::get().to(handlers::get_submissions), // Protected
|
||||
)
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()
|
||||
|
@ -1,7 +1,35 @@
|
||||
import type { Form, Submission, LoginCredentials } from './types';
|
||||
|
||||
const API_BASE_URL = 'http://127.0.0.1:8080';
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// 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).
|
||||
@ -38,8 +66,13 @@ async function authenticatedRequest(endpoint: string, options: RequestInit): Pro
|
||||
* @returns The ID of the created form.
|
||||
*/
|
||||
export async function createForm(name: string, fields: unknown): Promise<string> {
|
||||
return await authenticatedRequest('/forms', {
|
||||
const token = getAuthToken(); // Get the token from storage
|
||||
const response = await fetch(`${API_BASE_URL}/forms`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}` // Add token to the headers
|
||||
},
|
||||
body: JSON.stringify({ name, fields })
|
||||
});
|
||||
}
|
||||
@ -48,17 +81,21 @@ export async function createForm(name: string, fields: unknown): Promise<string>
|
||||
* Get all forms (authenticated).
|
||||
* @returns An array of forms.
|
||||
*/
|
||||
export async function getForms(): Promise<Form[]> {
|
||||
return await authenticatedRequest('/forms', { method: 'GET' });
|
||||
}
|
||||
export async function getForms(): Promise<unknown[]> {
|
||||
const token = getAuthToken(); // Get the token from storage
|
||||
const response = await fetch(`${API_BASE_URL}/forms`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}` // Add token to the headers
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all submissions for a specific form (authenticated).
|
||||
* @param formId The ID of the form.
|
||||
* @returns An array of submissions for the form.
|
||||
*/
|
||||
export async function getSubmissions(formId: string): Promise<Submission[]> {
|
||||
return await authenticatedRequest(`/forms/${formId}/submissions`, { method: 'GET' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching forms: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,10 +105,12 @@ export async function getSubmissions(formId: string): Promise<Submission[]> {
|
||||
* @returns The ID of the created submission.
|
||||
*/
|
||||
export async function submitForm(formId: string, data: unknown): Promise<string> {
|
||||
const token = getAuthToken(); // Get the token from storage
|
||||
const response = await fetch(`${API_BASE_URL}/forms/${formId}/submissions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}` // Add token to the headers
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
@ -88,42 +127,50 @@ export async function submitForm(formId: string, data: unknown): Promise<string>
|
||||
* @param credentials The login credentials (username and password).
|
||||
* @returns The generated JWT token if successful.
|
||||
*/
|
||||
export async function adminLogin(credentials: LoginCredentials): Promise<string> {
|
||||
const response = await fetch(`${API_BASE_URL}/admin/login`, {
|
||||
method: 'POST',
|
||||
export async function getSubmissions(formId: string): Promise<unknown[]> {
|
||||
const token = getAuthToken(); // Get the token from storage
|
||||
const response = await fetch(`${API_BASE_URL}/forms/${formId}/submissions`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(credentials)
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}` // Add token to the headers
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error during admin login: ${response.statusText}`);
|
||||
throw new Error(`Error fetching submissions: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
localStorage.setItem('authToken', data.token); // Store token locally
|
||||
return data.token;
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new admin user.
|
||||
* @param user The login credentials for the admin user.
|
||||
* @returns A success message upon creation.
|
||||
* Login and get the authentication token.
|
||||
* @param username The username.
|
||||
* @param password The password.
|
||||
* @returns The authentication token.
|
||||
*/
|
||||
export async function createAdmin(user: LoginCredentials): Promise<string> {
|
||||
const response = await fetch(`${API_BASE_URL}/admin/create`, {
|
||||
export async function login(username: string, password: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE_URL}/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(user)
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error creating admin user: ${response.statusText}`);
|
||||
throw new Error(`Error logging in: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.message;
|
||||
const { token } = await response.json();
|
||||
setAuthToken(token); // Store the token in localStorage
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
delAuthToken();
|
||||
}
|
||||
|
||||
export function loggedIn() {
|
||||
return localStorage.getItem('auth_token') !== null;
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
@ -1,8 +1,8 @@
|
||||
import session from "$lib/session.svelte";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { loggedIn } from '$lib/api';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export async function load() {
|
||||
if (!session.loggedIn()) {
|
||||
redirect(307, "/login");
|
||||
if (!loggedIn()) {
|
||||
redirect(307, '/login');
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,12 @@
|
||||
let fields: FormField[] = [];
|
||||
|
||||
function addField() {
|
||||
// Use a new array assignment to trigger reactivity
|
||||
fields = [...fields, { label: '', name: '', field_type: 'text' }];
|
||||
}
|
||||
|
||||
function removeField(index: number) {
|
||||
// Reassign to trigger reactivity
|
||||
fields = fields.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
@ -25,23 +27,26 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h1>Create Form</h1>
|
||||
<h1>Create Form</h1>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-name">Form Name</label>
|
||||
<input id="form-name" type="text" bind:value={name} placeholder="Enter form name" />
|
||||
</div>
|
||||
<label>
|
||||
Form Name:
|
||||
<input type="text" bind:value={name} placeholder="Enter form name" />
|
||||
</label>
|
||||
|
||||
<h2>Fields</h2>
|
||||
{#each fields as field, i}
|
||||
<div class="field-container">
|
||||
<h2>Fields</h2>
|
||||
{#each fields as field, i}
|
||||
<div>
|
||||
<label>
|
||||
Field Label
|
||||
Label:
|
||||
<input type="text" bind:value={field.label} placeholder="Enter field label" />
|
||||
</label>
|
||||
<label>
|
||||
Field Type
|
||||
Name:
|
||||
<input type="text" bind:value={field.name} placeholder="Enter field name" />
|
||||
</label>
|
||||
<label>
|
||||
Type:
|
||||
<select bind:value={field.field_type}>
|
||||
<option value="text">Text</option>
|
||||
<option value="number">Number</option>
|
||||
@ -49,101 +54,9 @@
|
||||
<option value="textarea">Textarea</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="button remove-button" on:click={() => removeField(i)}> Remove </button>
|
||||
<button on:click={() => removeField(i)}>Remove</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
<div class="actions">
|
||||
<button class="button" on:click={addField}> Add Field </button>
|
||||
<button class="button" on:click={saveForm} disabled={!name || fields.length === 0}>
|
||||
Save Form
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.field-container {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: #42b983;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #42b983;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.button:hover:not(:disabled) {
|
||||
background-color: #3aa876;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
background-color: #dc3545;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.remove-button:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
</style>
|
||||
<button on:click={addField}>Add Field</button>
|
||||
<button on:click={saveForm} disabled={!name || fields.length === 0}> Save Form </button>
|
||||
|
@ -1,179 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getForms, getSubmissions, submitForm } from '../../../lib/api';
|
||||
import type { Form, Submission } from '../../../lib/types';
|
||||
import { getForms, getSubmissions, submitForm } from '$lib/api';
|
||||
import type { Form, Submission } from '$lib/types';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let params: { id: string };
|
||||
|
||||
let form: Form | null = null;
|
||||
let submissions: Submission[] = [];
|
||||
console.log('params.id:', params);
|
||||
let form: any | null = null;
|
||||
let submissions: any[] = [];
|
||||
let responseData: Record<string, any> = {};
|
||||
let isSubmitting = false;
|
||||
|
||||
onMount(async () => {
|
||||
const { id } = $page.params;
|
||||
const { id } = $page.params; // Use $page.params to access route parameters
|
||||
if (id) {
|
||||
form = await getForms().then((forms) => forms.find((f: Form) => f.id === id) || null);
|
||||
form = await getForms().then((forms) => forms.find((f: any) => f.id === id) || null);
|
||||
submissions = await getSubmissions(id);
|
||||
} else {
|
||||
console.error('Route parameter id is missing');
|
||||
}
|
||||
});
|
||||
|
||||
async function submitResponse() {
|
||||
if (isSubmitting) return;
|
||||
|
||||
isSubmitting = true;
|
||||
try {
|
||||
const { id } = $page.params;
|
||||
const { id } = $page.params; // Use $page.params to access route parameters
|
||||
await submitForm(id, responseData);
|
||||
submissions = await getSubmissions(params.id);
|
||||
responseData = {};
|
||||
alert('Response submitted successfully!');
|
||||
} catch (error) {
|
||||
alert('Failed to submit response. Please try again.');
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatSubmissionData(data: Record<string, any>): string {
|
||||
return Object.entries(data)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(' | ');
|
||||
submissions = await getSubmissions(params.id); // Refresh submissions
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#if form}
|
||||
<h1>{form.name}</h1>
|
||||
<!-- <div class="form-container">
|
||||
<h1>{form?.name}</h1>
|
||||
|
||||
{#if form}
|
||||
<form on:submit|preventDefault={submitResponse}>
|
||||
{#each form.fields as field}
|
||||
<div class="field-group">
|
||||
<label for={field.name}>{field.label}</label>
|
||||
{#if field.field_type === 'textarea'}
|
||||
<textarea id={field.name} bind:value={responseData[field.name]}></textarea>
|
||||
{:else}
|
||||
<input
|
||||
id={field.name}
|
||||
type={field.field_type}
|
||||
bind:value={responseData[field.name]}
|
||||
/>
|
||||
<div>
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label>{field.label}</label>
|
||||
{#if field.field_type === 'text'}
|
||||
<input type="text" bind:value={responseData[field.name]} />
|
||||
{:else if field.field_type === 'number'}
|
||||
<input type="number" bind:value={responseData[field.name]} />
|
||||
{:else if field.field_type === 'date'}
|
||||
<input type="date" bind:value={responseData[field.name]} />
|
||||
{:else if field.field_type === 'textarea'}
|
||||
<textarea bind:value={responseData[field.name]}></textarea>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<button type="submit" class="submit-button" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit Response'}
|
||||
</button>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</div> -->
|
||||
|
||||
<h2>Submissions</h2>
|
||||
{#if submissions.length > 0}
|
||||
<ul class="submissions-list">
|
||||
<ul>
|
||||
{#each submissions as submission}
|
||||
<li class="submission-item">
|
||||
{formatSubmissionData(submission.data)}
|
||||
</li>
|
||||
<li>{JSON.stringify(submission.data)}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p>No submissions yet.</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="loading">Loading...</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: #f8f9fa;
|
||||
padding: 2rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.field-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: #42b983;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #42b983;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.submit-button:hover:not(:disabled) {
|
||||
background-color: #3aa876;
|
||||
}
|
||||
|
||||
.submit-button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.submissions-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.submission-item {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #42b983;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 2rem;
|
||||
}
|
||||
</style>
|
||||
{:else}
|
||||
<p>Loading...</p>
|
||||
{/if}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getForms } from '../lib/api';
|
||||
import type { Form } from '../lib/types';
|
||||
import { getForms } from '../../../lib/api';
|
||||
import type { Form } from '../../../lib/types';
|
||||
|
||||
let forms: Form[] = [];
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children?.()}
|
||||
{@render children()}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import session from '$lib/session.svelte';
|
||||
import { loggedIn } from '$lib/api';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export async function load() {
|
||||
if (!session.loggedIn()) {
|
||||
redirect(307, '/login');
|
||||
}
|
||||
const page = loggedIn() ? '/main' : '/login';
|
||||
redirect(307, page);
|
||||
}
|
||||
|
@ -1,103 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { adminLogin } from '$lib/api';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import session from '$lib/session.svelte';
|
||||
import { login } from '$lib/api';
|
||||
|
||||
let username = '';
|
||||
let password = '';
|
||||
let errorMessage = '';
|
||||
let loading = false;
|
||||
const dispatch = createEventDispatcher();
|
||||
let isLoading = false;
|
||||
|
||||
async function handleSubmit(event: any) {
|
||||
event.preventDefault();
|
||||
errorMessage = '';
|
||||
loading = true;
|
||||
const handleLogin = async () => {
|
||||
isLoading = true;
|
||||
errorMessage = ''; // Reset any previous error message
|
||||
|
||||
try {
|
||||
const token = await adminLogin({ username, password });
|
||||
|
||||
// Store session data
|
||||
session.login({
|
||||
username,
|
||||
password_hash: token // Assuming the token acts as a password hash for session purposes
|
||||
});
|
||||
|
||||
dispatch('login', { username, token });
|
||||
alert('Login successful!');
|
||||
await login(username, password);
|
||||
// If successful, you can redirect the user to another page or show a success message
|
||||
window.location.href = '/main'; // Example redirection after login
|
||||
} catch (error: any) {
|
||||
errorMessage = error.message || 'Login failed. Please try again.';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
isLoading = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<form on:submit={handleSubmit} class="login-form">
|
||||
<div class="login-container">
|
||||
<h2>Login</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
class="input-field"
|
||||
bind:value={username}
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
/>
|
||||
<input type="text" id="username" bind:value={username} placeholder="Enter your username" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
class="input-field"
|
||||
bind:value={password}
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
|
||||
<button type="submit" class="submit-btn" disabled={loading}>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
<input type="password" id="password" bind:value={password} placeholder="Enter your password" />
|
||||
</div>
|
||||
|
||||
{#if errorMessage}
|
||||
<div class="error-message">{errorMessage}</div>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: red;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
<button class="submit-button" on:click={handleLogin} disabled={isLoading}>
|
||||
{#if isLoading}
|
||||
<div class="loading">
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
{:else}
|
||||
Login
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import session from '$lib/session.svelte';
|
||||
import { loggedIn } from '$lib/api';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export async function load() {
|
||||
if (session.loggedIn()) {
|
||||
if (loggedIn()) {
|
||||
redirect(307, '/');
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user