Compare commits

..

7 Commits

Author SHA1 Message Date
Mohamad
542d8583ab enhanced login?
Some checks failed
Build and Deploy / build (push) Failing after 6s
2024-12-30 14:54:16 +01:00
Mohamad
50d03a5387 bugfix: circular dep 2024-12-30 14:53:16 +01:00
Mohamad
227692b68e authtoken and stuff 2024-12-30 14:53:00 +01:00
Mohamad
b110821483 session edited to resemble api 2024-12-30 14:52:46 +01:00
Mohamad
2bf5ae58e5 admin login handler logic reworked 2024-12-30 14:52:11 +01:00
Mohamad
e2abd2ca83 simple default user generation 2024-12-30 14:51:34 +01:00
Mohamad
626b01e56b updated deps 2024-12-30 14:51:12 +01:00
8 changed files with 129 additions and 101 deletions

1
backend/Cargo.lock generated
View File

@ -587,6 +587,7 @@ dependencies = [
"futures",
"jsonwebtoken",
"log",
"rand_core",
"rusqlite",
"serde",
"serde_json",

View File

@ -15,4 +15,5 @@ env_logger = "0.10" # Check for the latest version
log = "0.4"
jsonwebtoken = "9"
argon2 = { version = "0.5", features = ["password-hash"] }
futures = "0.3"
futures = "0.3"
rand_core = "0.6.4"

View File

@ -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)
}

View File

@ -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" })),
}
}

View File

@ -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;
}

View File

@ -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 };

View File

@ -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');
}
}

View File

@ -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>