Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 130 additions & 7 deletions apps/web/src/lib/stores/auth.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ============================================================================
// SIMPLE AUTH STORE
// AUTH STORE WITH PASSWORD SUPPORT
// ============================================================================
// Simplified Svelte 5 Runes auth store for local-only P2P app
// Svelte 5 Runes auth store with password authentication
// ============================================================================

import type { AuthState, UserSession } from "$auth/index";
Expand All @@ -10,6 +10,51 @@ import type { AuthState, UserSession } from "$auth/index";
let currentSession: UserSession | null = $state(null);
let authState: AuthState = $state({ status: "unauthenticated" });

// Store for user credentials (username -> password hash)
const USER_STORAGE_KEY = "locanote_users";

// ============================================================================
// PASSWORD HASHING (Simple hash for local-only app)
// ============================================================================

async function hashPassword(password: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}

// ============================================================================
// USER STORAGE
// ============================================================================

interface StoredUser {
username: string;
passwordHash: string;
createdAt: number;
}

function getStoredUsers(): Record<string, StoredUser> {
if (typeof window === "undefined") return {};
const stored = localStorage.getItem(USER_STORAGE_KEY);
return stored ? JSON.parse(stored) : {};
}

function saveUser(username: string, passwordHash: string) {
const users = getStoredUsers();
users[username.toLowerCase()] = {
username,
passwordHash,
createdAt: Date.now(),
};
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(users));
}

function getUser(username: string): StoredUser | undefined {
return getStoredUsers()[username.toLowerCase()];
}

// ============================================================================
// METHODS
// ============================================================================
Expand Down Expand Up @@ -37,15 +82,42 @@ export const auth = {
return authState;
},

// Login
login(username: string): { success: boolean; error?: string } {
// Login with password
async login(
username: string,
password: string,
): Promise<{ success: boolean; error?: string }> {
if (!username || username.trim().length < 2) {
return {
success: false,
error: "Username must be at least 2 characters",
};
}

if (!password || password.length < 6) {
return {
success: false,
error: "Password must be at least 6 characters",
};
}

// Verify user exists and password matches
const user = getUser(username);
if (!user) {
return {
success: false,
error: "Invalid username or password",
};
}

const passwordHash = await hashPassword(password);
if (passwordHash !== user.passwordHash) {
return {
success: false,
error: "Invalid username or password",
};
}

const userId = `${username.trim().toLowerCase().replace(/\s+/g, "_")}_${Date.now()}`;
const session: UserSession = {
userId,
Expand All @@ -61,9 +133,60 @@ export const auth = {
return { success: true };
},

// Register (same as login for MVP)
register(username: string): { success: boolean; error?: string } {
return this.login(username);
// Register with password
async register(
username: string,
password: string,
confirmPassword: string,
): Promise<{ success: boolean; error?: string }> {
if (!username || username.trim().length < 2) {
return {
success: false,
error: "Username must be at least 2 characters",
};
}

if (!password || password.length < 6) {
return {
success: false,
error: "Password must be at least 6 characters",
};
}

if (password !== confirmPassword) {
return {
success: false,
error: "Passwords do not match",
};
}

// Check if user already exists
const existingUser = getUser(username);
if (existingUser) {
return {
success: false,
error: "Username already exists",
};
}

// Hash and store password
const passwordHash = await hashPassword(password);
saveUser(username, passwordHash);

// Create session
const userId = `${username.trim().toLowerCase().replace(/\s+/g, "_")}_${Date.now()}`;
const session: UserSession = {
userId,
username: username.trim(),
loggedInAt: Date.now(),
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
};

localStorage.setItem("locanote_session", JSON.stringify(session));
currentSession = session;
authState = { status: "authenticated", session };

return { success: true };
},

// Logout
Expand Down
61 changes: 49 additions & 12 deletions apps/web/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ LANDING PAGE - Beautiful Glass Design
<script lang="ts">
import { goto } from "$app/navigation";
import { onMount } from "svelte";
import { auth } from "$stores/auth.svelte";

let username = $state("");
let password = $state("");
let confirmPassword = $state("");
let isLoading = $state(false);
let error = $state("");
let isRegister = $state(false);
Expand Down Expand Up @@ -43,7 +46,7 @@ LANDING PAGE - Beautiful Glass Design
}
});

function handleSubmit() {
async function handleSubmit() {
if (!username.trim()) {
error = "Please enter a username";
return;
Expand All @@ -54,18 +57,28 @@ LANDING PAGE - Beautiful Glass Design
return;
}

if (!password || password.length < 6) {
error = "Password must be at least 6 characters";
return;
}

isLoading = true;
error = "";

const session = {
userId: username.toLowerCase().replace(/\s+/g, "_") + "_" + Date.now(),
username: username.trim(),
loggedInAt: Date.now(),
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
};
let result;
if (isRegister) {
result = await auth.register(username.trim(), password, confirmPassword);
} else {
result = await auth.login(username.trim(), password);
}

isLoading = false;

localStorage.setItem("locanote_session", JSON.stringify(session));
goto("/app", { replaceState: true });
if (result.success) {
goto("/app", { replaceState: true });
} else {
error = result.error || "Authentication failed";
}
}

function handleKeydown(e: KeyboardEvent) {
Expand Down Expand Up @@ -128,12 +141,36 @@ LANDING PAGE - Beautiful Glass Design
type="text"
bind:value={username}
onkeydown={handleKeydown}
placeholder="Choose a username"
placeholder="Username"
class="input"
autocomplete="username"
/>
</div>

<div class="input-group">
<input
type="password"
bind:value={password}
onkeydown={handleKeydown}
placeholder="Password"
class="input"
autocomplete="off"
autocomplete={isRegister ? "new-password" : "current-password"}
/>
</div>

{#if isRegister}
<div class="input-group">
<input
type="password"
bind:value={confirmPassword}
onkeydown={handleKeydown}
placeholder="Confirm Password"
class="input"
autocomplete="new-password"
/>
</div>
{/if}

{#if error}
<div class="error">{error}</div>
{/if}
Expand All @@ -142,7 +179,7 @@ LANDING PAGE - Beautiful Glass Design
{#if isLoading}
<span class="spinner"></span>
{:else}
{isRegister ? "Create Account" : "Continue"}
{isRegister ? "Create Account" : "Sign In"}
{/if}
</button>
</div>
Expand Down