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