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", "futures",
"jsonwebtoken", "jsonwebtoken",
"log", "log",
"rand_core",
"rusqlite", "rusqlite",
"serde", "serde",
"serde_json", "serde_json",

View File

@ -15,4 +15,5 @@ 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"

View File

@ -1,8 +1,11 @@
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,
@ -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) Ok(conn)
} }

View File

@ -108,61 +108,73 @@ 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 = db.lock().unwrap(); let conn = match db.lock() {
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) => return HttpResponse::InternalServerError().body(format!("Error: {}", e)), Err(e) => {
return HttpResponse::InternalServerError().body(format!("Database error: {}", e))
}
}; };
let admin: Option<AdminUser> = stmt let admin: Option<AdminUser> = match stmt.query_row([&credentials.username], |row| {
.query_row([&credentials.username], |row| { Ok(AdminUser {
Ok(AdminUser { username: row.get(0)?,
username: row.get(0)?, password_hash: row.get(1)?,
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 = PasswordHash::new(&user.password_hash).unwrap(); let parsed_hash = match PasswordHash::new(&user.password_hash) {
let argon2 = Argon2::default(); Ok(hash) => hash,
Err(_) => {
return HttpResponse::InternalServerError()
.body("Invalid password hash format in database")
}
};
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 = SystemTime::now() let expiration = match SystemTime::now().duration_since(UNIX_EPOCH) {
.duration_since(UNIX_EPOCH) Ok(duration) => duration.as_secs() as usize + 24 * 3600,
.unwrap() Err(_) => return HttpResponse::InternalServerError().body("System time error"),
.as_secs() as usize };
+ 24 * 3600;
let claims = Claims { let claims = Claims {
sub: user.username, sub: user.username,
exp: expiration, exp: expiration,
}; };
let token = encode( let token = match encode(
&Header::default(), &Header::default(),
&claims, &claims,
&EncodingKey::from_secret("your-secret-key".as_ref()), &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 })) HttpResponse::Ok().json(json!({ "token": token }))
} else { } else {
HttpResponse::Unauthorized().json(json!({ HttpResponse::Unauthorized().json(json!({ "error": "Invalid credentials" }))
"error": "Invalid credentials"
}))
} }
} }
None => HttpResponse::Unauthorized().json(json!({ None => HttpResponse::Unauthorized().json(json!({ "error": "Invalid credentials" })),
"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'; 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 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> {
const response = await fetch(`${API_BASE_URL}/forms`, { return await authenticatedRequest('/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. * Get all forms (authenticated).
* @returns An array of forms. * @returns An array of forms.
*/ */
export async function getForms(): Promise<Form[]> { export async function getForms(): Promise<Form[]> {
const response = await fetch(`${API_BASE_URL}/forms`, { return await authenticatedRequest('/forms', { method: 'GET' });
method: 'GET',
headers: {
Accept: 'application/json'
}
});
if (!response.ok) {
throw new Error(`Error fetching forms: ${response.statusText}`);
}
return await response.json();
} }
/** /**
* 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 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.
@ -65,26 +83,6 @@ 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).
@ -104,7 +102,8 @@ export async function adminLogin(credentials: LoginCredentials): Promise<string>
} }
const data = await response.json(); 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(); 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'; 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(key) !== null; return localStorage.getItem('authToken') !== null;
} }
function name() { export default { login, logout, loggedIn };
return localStorage.getItem(key2) ?? '';
}
function auth() {
return localStorage.getItem(key);
}
export default { login, logout, loggedIn, name, auth };

View File

@ -2,6 +2,7 @@ import session from '$lib/session.svelte';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
export async function load() { export async function load() {
const page = session.loggedIn() ? '/' : '/login'; if (!session.loggedIn()) {
redirect(307, page); redirect(307, '/login');
}
} }

View File

@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { adminLogin } from '$lib/api'; import { adminLogin } from '$lib/api';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import session from '$lib/session.svelte';
let username = ''; let username = '';
let password = ''; let password = '';
let errorMessage = ''; let errorMessage = '';
@ -14,7 +16,14 @@
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.';
@ -52,10 +61,6 @@
{#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>
@ -95,11 +100,4 @@
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>