Compare commits

...

10 Commits

Author SHA1 Message Date
Mohamad.Elsena
e225b6dcad fixed imports 2025-01-02 14:36:59 +01:00
Mohamad.Elsena
24e3421107 deleted 2025-01-02 14:35:23 +01:00
Mohamad.Elsena
a20bfd2101 session managment 2025-01-02 14:35:14 +01:00
Mohamad.Elsena
9259cdd84c init - basic toke-based auth 2025-01-02 14:34:55 +01:00
Mohamad.Elsena
da418169c6 moved 2025-01-02 14:34:25 +01:00
Mohamad.Elsena
a30f14a0d4 added
- better error msgs
- default user
2025-01-02 14:33:49 +01:00
Mohamad.Elsena
25db5f108d deps+test db 2025-01-02 14:32:59 +01:00
Mohamad.Elsena
8b12522924 test db 2025-01-02 13:20:36 +01:00
Mohamad.Elsena
1b21ec4f32 added basic auth 2025-01-02 13:20:27 +01:00
Mohamad.Elsena
793e136087 basic backend auth fr 2025-01-02 13:20:10 +01:00
20 changed files with 485 additions and 62 deletions

122
backend/Cargo.lock generated
View File

@ -68,7 +68,7 @@ dependencies = [
"actix-service",
"actix-utils",
"ahash",
"base64",
"base64 0.22.1",
"bitflags",
"brotli",
"bytes",
@ -281,6 +281,12 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anyhow"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
[[package]]
name = "autocfg"
version = "1.4.0"
@ -302,12 +308,30 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bcrypt"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7e7c93a3fb23b2fdde989b2c9ec4dd153063ec81f408507f84c090cd91c6641"
dependencies = [
"base64 0.13.1",
"blowfish",
"getrandom",
"zeroize",
]
[[package]]
name = "bitflags"
version = "2.6.0"
@ -323,6 +347,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"
@ -382,6 +416,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"
@ -542,7 +586,10 @@ dependencies = [
"actix-cors",
"actix-files",
"actix-web",
"anyhow",
"bcrypt",
"env_logger",
"futures",
"log",
"rusqlite",
"serde",
@ -550,12 +597,65 @@ dependencies = [
"uuid",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
@ -574,8 +674,13 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
@ -848,6 +953,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"
@ -1664,6 +1778,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"

View File

@ -11,5 +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"
futures = "0.3"
bcrypt = "0.13"
anyhow = "1.0"

Binary file not shown.

36
backend/src/auth.rs Normal file
View 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")))
}
}

View File

@ -1,7 +1,10 @@
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")?;
conn.execute(
"CREATE TABLE IF NOT EXISTS forms (
@ -24,5 +27,96 @@ pub fn init_db() -> Result<Connection> {
[],
)?;
// Add a table for users
conn.execute(
"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
)",
[],
)?;
// 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(())
}

View File

@ -1,17 +1,73 @@
use actix_web::{web, HttpResponse, Responder};
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use uuid::Uuid;
use crate::models::{Form, Submission};
use crate::auth::Auth;
// Create a new form
// 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();
@ -24,9 +80,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,
@ -39,41 +96,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 =
@ -88,10 +126,11 @@ 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)
}

View File

@ -1,22 +1,19 @@
use actix_cors::Cors;
// use actix_files as fs;
use actix_web::{web, App, HttpServer};
use std::sync::{Arc, Mutex};
mod auth;
mod db;
mod handlers;
mod models;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Initialize the database connection
env_logger::init();
let db = Arc::new(Mutex::new(
db::init_db().expect("Failed to initialize the database"),
));
// Start the Actix-Web server
HttpServer::new(move || {
App::new()
.wrap(
@ -26,16 +23,16 @@ async fn main() -> std::io::Result<()> {
.allow_any_method(),
)
.app_data(web::Data::new(db.clone()))
// .service(fs::Files::new("/", "./frontend/public").index_file("index.html"))
.route("/forms", web::post().to(handlers::create_form))
.route("/forms", web::get().to(handlers::get_forms))
.route("/login", web::post().to(handlers::login)) // Public: Login
.route(
"/forms/{id}/submissions",
web::post().to(handlers::submit_form),
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),
web::get().to(handlers::get_submissions), // Protected
)
})
.bind("127.0.0.1:8080")?

0
frontend/src/app.css Normal file
View File

View File

@ -1,6 +1,20 @@
// api.ts
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');
}
/**
* Create a new form.
* @param name The name of the form.
@ -8,10 +22,12 @@ const API_BASE_URL = 'http://127.0.0.1:8080';
* @returns The ID of the created form.
*/
export async function createForm(name: string, fields: unknown): Promise<string> {
const token = getAuthToken(); // Get the token from storage
const response = await fetch(`${API_BASE_URL}/forms`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` // Add token to the headers
},
body: JSON.stringify({ name, fields })
});
@ -28,10 +44,12 @@ export async function createForm(name: string, fields: unknown): Promise<string>
* @returns An array of forms.
*/
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'
Accept: 'application/json',
Authorization: `Bearer ${token}` // Add token to the headers
}
});
@ -49,10 +67,12 @@ export async function getForms(): Promise<unknown[]> {
* @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)
});
@ -70,10 +90,12 @@ export async function submitForm(formId: string, data: unknown): Promise<string>
* @returns An array of submissions for the form.
*/
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: {
Accept: 'application/json'
Accept: 'application/json',
Authorization: `Bearer ${token}` // Add token to the headers
}
});
@ -83,3 +105,34 @@ export async function getSubmissions(formId: string): Promise<unknown[]> {
return await response.json();
}
/**
* Login and get the authentication token.
* @param username The username.
* @param password The password.
* @returns The authentication token.
*/
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({ username, password })
});
if (!response.ok) {
throw new Error(`Error logging in: ${response.statusText}`);
}
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;
}

View File

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@ -0,0 +1,8 @@
import { loggedIn } from '$lib/api';
import { redirect } from '@sveltejs/kit';
export async function load() {
if (!loggedIn()) {
redirect(307, '/login');
}
}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { createForm } from '../../lib/api';
import type { FormField } from '../../lib/types';
import { createForm } from '../../../lib/api';
import type { FormField } from '../../../lib/types';
let name = '';
let fields: FormField[] = [];

View File

@ -1,7 +1,7 @@
<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 };

View File

@ -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: any;

View File

@ -0,0 +1,7 @@
<script lang="ts">
import '../app.css';
let { children } = $props();
</script>
{@render children()}

View File

@ -0,0 +1 @@
export const ssr = false;

View File

@ -0,0 +1,7 @@
import { loggedIn } from '$lib/api';
import { redirect } from '@sveltejs/kit';
export async function load() {
const page = loggedIn() ? '/main' : '/login';
redirect(307, page);
}

View File

@ -0,0 +1,51 @@
<script lang="ts">
import { login } from '$lib/api';
let username = '';
let password = '';
let errorMessage = '';
let isLoading = false;
const handleLogin = async () => {
isLoading = true;
errorMessage = ''; // Reset any previous error message
try {
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 {
isLoading = false;
}
};
</script>
<div class="login-container">
<h2>Login</h2>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" bind:value={username} placeholder="Enter your username" />
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" bind:value={password} placeholder="Enter your password" />
</div>
{#if errorMessage}
<div class="error-message">{errorMessage}</div>
{/if}
<button class="submit-button" on:click={handleLogin} disabled={isLoading}>
{#if isLoading}
<div class="loading">
<span>Loading...</span>
</div>
{:else}
Login
{/if}
</button>
</div>

View File

@ -0,0 +1,8 @@
import { loggedIn } from '$lib/api';
import { redirect } from '@sveltejs/kit';
export async function load() {
if (loggedIn()) {
redirect(307, '/');
}
}