Compare commits
No commits in common. "542d8583abab4099916b01121ce2ea5bf5320b28" and "827809c514cfeba5e928791897e6f67dabc283da" have entirely different histories.
542d8583ab
...
827809c514
1
backend/Cargo.lock
generated
1
backend/Cargo.lock
generated
@ -587,7 +587,6 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"log",
|
"log",
|
||||||
"rand_core",
|
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -15,5 +15,4 @@ env_logger = "0.10" # Check for the latest version
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
jsonwebtoken = "9"
|
jsonwebtoken = "9"
|
||||||
argon2 = { version = "0.5", features = ["password-hash"] }
|
argon2 = { version = "0.5", features = ["password-hash"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
rand_core = "0.6.4"
|
|
@ -1,11 +1,8 @@
|
|||||||
use argon2::{password_hash::SaltString, Algorithm, Argon2, Params, PasswordHasher, Version};
|
|
||||||
use rand_core::OsRng;
|
|
||||||
use rusqlite::{Connection, Result};
|
use rusqlite::{Connection, Result};
|
||||||
|
|
||||||
pub fn init_db() -> Result<Connection> {
|
pub fn init_db() -> Result<Connection> {
|
||||||
let conn = Connection::open("form_data.db")?;
|
let conn = Connection::open("form_data.db")?;
|
||||||
|
|
||||||
// Create tables
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS forms (
|
"CREATE TABLE IF NOT EXISTS forms (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@ -35,29 +32,5 @@ pub fn init_db() -> Result<Connection> {
|
|||||||
[],
|
[],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// 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()],
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
@ -108,73 +108,61 @@ pub async fn admin_login(
|
|||||||
db: web::Data<Arc<Mutex<Connection>>>,
|
db: web::Data<Arc<Mutex<Connection>>>,
|
||||||
credentials: web::Json<LoginCredentials>,
|
credentials: web::Json<LoginCredentials>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let conn = match db.lock() {
|
let conn = db.lock().unwrap();
|
||||||
Ok(conn) => conn,
|
|
||||||
Err(_) => return HttpResponse::InternalServerError().body("Database lock error"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut stmt =
|
let mut stmt =
|
||||||
match conn.prepare("SELECT username, password_hash FROM admin_users WHERE username = ?1") {
|
match conn.prepare("SELECT username, password_hash FROM admin_users WHERE username = ?1") {
|
||||||
Ok(stmt) => stmt,
|
Ok(stmt) => stmt,
|
||||||
Err(e) => {
|
Err(e) => return HttpResponse::InternalServerError().body(format!("Error: {}", e)),
|
||||||
return HttpResponse::InternalServerError().body(format!("Database error: {}", e))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let admin: Option<AdminUser> = match stmt.query_row([&credentials.username], |row| {
|
let admin: Option<AdminUser> = stmt
|
||||||
Ok(AdminUser {
|
.query_row([&credentials.username], |row| {
|
||||||
username: row.get(0)?,
|
Ok(AdminUser {
|
||||||
password_hash: row.get(1)?,
|
username: row.get(0)?,
|
||||||
|
password_hash: row.get(1)?,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}) {
|
.ok();
|
||||||
Ok(admin) => Some(admin),
|
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => None, // No user found
|
|
||||||
Err(e) => return HttpResponse::InternalServerError().body(format!("Query error: {}", e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
match admin {
|
match admin {
|
||||||
Some(user) => {
|
Some(user) => {
|
||||||
let parsed_hash = match PasswordHash::new(&user.password_hash) {
|
let parsed_hash = PasswordHash::new(&user.password_hash).unwrap();
|
||||||
Ok(hash) => hash,
|
|
||||||
Err(_) => {
|
|
||||||
return HttpResponse::InternalServerError()
|
|
||||||
.body("Invalid password hash format in database")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let argon2 = Argon2::default();
|
let argon2 = Argon2::default();
|
||||||
|
|
||||||
let is_valid = argon2
|
let is_valid = argon2
|
||||||
.verify_password(credentials.password.as_bytes(), &parsed_hash)
|
.verify_password(credentials.password.as_bytes(), &parsed_hash)
|
||||||
.is_ok();
|
.is_ok();
|
||||||
|
|
||||||
if is_valid {
|
if is_valid {
|
||||||
let expiration = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
let expiration = SystemTime::now()
|
||||||
Ok(duration) => duration.as_secs() as usize + 24 * 3600,
|
.duration_since(UNIX_EPOCH)
|
||||||
Err(_) => return HttpResponse::InternalServerError().body("System time error"),
|
.unwrap()
|
||||||
};
|
.as_secs() as usize
|
||||||
|
+ 24 * 3600;
|
||||||
|
|
||||||
let claims = Claims {
|
let claims = Claims {
|
||||||
sub: user.username,
|
sub: user.username,
|
||||||
exp: expiration,
|
exp: expiration,
|
||||||
};
|
};
|
||||||
|
|
||||||
let token = match encode(
|
let token = encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
&claims,
|
&claims,
|
||||||
&EncodingKey::from_secret("your-secret-key".as_ref()),
|
&EncodingKey::from_secret("your-secret-key".as_ref()),
|
||||||
) {
|
)
|
||||||
Ok(token) => token,
|
.unwrap();
|
||||||
Err(_) => {
|
|
||||||
return HttpResponse::InternalServerError().body("Token generation error")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpResponse::Ok().json(json!({ "token": token }))
|
HttpResponse::Ok().json(json!({ "token": token }))
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::Unauthorized().json(json!({ "error": "Invalid credentials" }))
|
HttpResponse::Unauthorized().json(json!({
|
||||||
|
"error": "Invalid credentials"
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => HttpResponse::Unauthorized().json(json!({ "error": "Invalid credentials" })),
|
None => HttpResponse::Unauthorized().json(json!({
|
||||||
|
"error": "Invalid credentials"
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,66 +3,48 @@ import type { Form, Submission, LoginCredentials } from './types';
|
|||||||
const API_BASE_URL = 'http://127.0.0.1:8080';
|
const API_BASE_URL = 'http://127.0.0.1:8080';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to make authenticated requests.
|
* Create a new form.
|
||||||
* @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).
|
|
||||||
* @param name The name of the form.
|
* @param name The name of the form.
|
||||||
* @param fields The fields of the form in JSON format.
|
* @param fields The fields of the form in JSON format.
|
||||||
* @returns The ID of the created form.
|
* @returns The ID of the created form.
|
||||||
*/
|
*/
|
||||||
export async function createForm(name: string, fields: unknown): Promise<string> {
|
export async function createForm(name: string, fields: unknown): Promise<string> {
|
||||||
return await authenticatedRequest('/forms', {
|
const response = await fetch(`${API_BASE_URL}/forms`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
body: JSON.stringify({ name, fields })
|
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.
|
* @returns An array of forms.
|
||||||
*/
|
*/
|
||||||
export async function getForms(): Promise<Form[]> {
|
export async function getForms(): Promise<Form[]> {
|
||||||
return await authenticatedRequest('/forms', { method: 'GET' });
|
const response = await fetch(`${API_BASE_URL}/forms`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error fetching forms: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all submissions for a specific form (authenticated).
|
* Submit a form.
|
||||||
* @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' });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit a form (unauthenticated).
|
|
||||||
* @param formId The ID of the form to submit.
|
* @param formId The ID of the form to submit.
|
||||||
* @param data The submission data in JSON format.
|
* @param data The submission data in JSON format.
|
||||||
* @returns The ID of the created submission.
|
* @returns The ID of the created submission.
|
||||||
@ -83,6 +65,26 @@ export async function submitForm(formId: string, data: unknown): Promise<string>
|
|||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Submission[]> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/forms/${formId}/submissions`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error fetching submissions: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin login to get a token.
|
* Admin login to get a token.
|
||||||
* @param credentials The login credentials (username and password).
|
* @param credentials The login credentials (username and password).
|
||||||
@ -102,8 +104,7 @@ export async function adminLogin(credentials: LoginCredentials): Promise<string>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
localStorage.setItem('authToken', data.token); // Store token locally
|
return data.token; // Assuming the response contains the token
|
||||||
return data.token;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -125,5 +126,5 @@ export async function createAdmin(user: LoginCredentials): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.message;
|
return data.message; // Assuming the response contains a success message
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,28 @@
|
|||||||
import type { AdminUser } from './types';
|
import type { AdminUser } from './types';
|
||||||
|
|
||||||
|
const key = 'user';
|
||||||
const key2 = 'username';
|
const key2 = 'username';
|
||||||
|
|
||||||
function login(user: AdminUser) {
|
function login(user: AdminUser) {
|
||||||
|
localStorage.setItem(key, btoa(`${user.username}:${user.password_hash}`));
|
||||||
localStorage.setItem(key2, user.username);
|
localStorage.setItem(key2, user.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
|
localStorage.removeItem(key);
|
||||||
localStorage.removeItem(key2);
|
localStorage.removeItem(key2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loggedIn() {
|
function loggedIn() {
|
||||||
return localStorage.getItem('authToken') !== null;
|
return localStorage.getItem(key) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { login, logout, loggedIn };
|
function name() {
|
||||||
|
return localStorage.getItem(key2) ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function auth() {
|
||||||
|
return localStorage.getItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { login, logout, loggedIn, name, auth };
|
||||||
|
@ -2,7 +2,6 @@ import session from '$lib/session.svelte';
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
if (!session.loggedIn()) {
|
const page = session.loggedIn() ? '/' : '/login';
|
||||||
redirect(307, '/login');
|
redirect(307, page);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { adminLogin } from '$lib/api';
|
import { adminLogin } from '$lib/api';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import session from '$lib/session.svelte';
|
|
||||||
|
|
||||||
let username = '';
|
let username = '';
|
||||||
let password = '';
|
let password = '';
|
||||||
let errorMessage = '';
|
let errorMessage = '';
|
||||||
@ -16,14 +14,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const token = await adminLogin({ username, password });
|
const token = await adminLogin({ username, password });
|
||||||
|
dispatch('login', { token }); // Dispatch the token to the parent component or handle it here
|
||||||
// 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!');
|
alert('Login successful!');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
errorMessage = error.message || 'Login failed. Please try again.';
|
errorMessage = error.message || 'Login failed. Please try again.';
|
||||||
@ -61,6 +52,10 @@
|
|||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<div class="error-message">{errorMessage}</div>
|
<div class="error-message">{errorMessage}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="loading">Loading...</div>
|
||||||
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -100,4 +95,11 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
Reference in New Issue
Block a user