refactor: Transition to authentication system and remove identity management

- Replace identity management with a new authentication system across the application.
- Update App.vue to integrate LoginDialog and remove PasswordDialog.
- Modify Navbar.vue to handle user authentication state and logout functionality.
- Enhance Home.vue to display user information upon login.
- Implement routing changes in index.ts to enforce authentication requirements for protected routes.
This commit is contained in:
padreug 2025-07-29 23:10:31 +02:00
parent 5ceb12ca3b
commit be4ab13b32
11 changed files with 1065 additions and 96 deletions

109
AUTHENTICATION.md Normal file
View file

@ -0,0 +1,109 @@
# Authentication System
This web application now uses LNBits username/password authentication instead of Nostr keypairs.
## Overview
The authentication system has been completely replaced with a traditional username/password system that integrates with LNBits. Users can now:
- Register new accounts with username and password
- Login with username/email and password
- Manage their profile information
- Logout securely
## Configuration
### Environment Variables
Create a `.env` file in the `web-app` directory with the following variables:
```env
# LNBits API Configuration
# Set this to your LNBits instance API URL
# Example: http://localhost:5000/api/v1 or https://your-lnbits-instance.com/api/v1
VITE_LNBITS_API_URL=/api/v1
# Enable debug logging for LNBits API calls
VITE_LNBITS_DEBUG=false
# App Configuration
VITE_APP_TITLE=Ario
VITE_APP_DESCRIPTION=Your secure platform for events and community management
```
### LNBits Setup
1. Ensure your LNBits instance is running and accessible
2. Make sure the username/password authentication method is enabled in LNBits
3. Configure CORS if your LNBits instance is on a different domain
## API Endpoints
The application uses the following LNBits API endpoints:
- `POST /api/v1/auth` - Login
- `POST /api/v1/auth/register` - Register new user
- `POST /api/v1/auth/logout` - Logout
- `GET /api/v1/auth` - Get current user
- `PUT /api/v1/auth/password` - Update password
- `PUT /api/v1/auth/update` - Update profile
## Components
### New Components
- `LoginDialog.vue` - Modal dialog for login/register
- `UserProfile.vue` - Display user information and logout
- `Login.vue` - Full-page login/register form
### Updated Components
- `App.vue` - Now uses new authentication system
- `Navbar.vue` - Shows user status and logout option
- `Home.vue` - Displays welcome message and user profile
## Authentication Flow
1. **App Initialization**: The app checks for existing authentication token on startup
2. **Route Protection**: Routes with `requiresAuth: true` redirect to login if not authenticated
3. **Login/Register**: Users can create accounts or login with existing credentials
4. **Token Management**: Access tokens are stored in localStorage and automatically included in API requests
5. **Logout**: Clears tokens and redirects to login page
## Security Features
- JWT tokens for session management
- Secure password handling (handled by LNBits)
- Automatic token refresh
- Route protection for authenticated pages
- Secure logout with token cleanup
## Migration from Nostr
The following components have been removed or replaced:
- `useIdentity.ts``useAuth.ts`
- `IdentityDialog.vue``LoginDialog.vue`
- `PasswordDialog.vue` → Integrated into `LoginDialog.vue`
- Nostr connection status → User authentication status
## Development
To run the application with the new authentication system:
1. Set up your LNBits instance
2. Configure the environment variables
3. Run the development server: `npm run dev`
4. Access the application and test login/register functionality
## Troubleshooting
### Common Issues
1. **CORS Errors**: Ensure your LNBits instance allows requests from your frontend domain
2. **Authentication Failures**: Check that username/password auth is enabled in LNBits
3. **API Connection**: Verify the `VITE_LNBITS_API_URL` is correct
### Debug Mode
Enable debug logging by setting `VITE_LNBITS_DEBUG=true` to see detailed API request/response information in the browser console.

View file

@ -1,57 +1,27 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { onMounted, ref } from 'vue'
import Navbar from '@/components/layout/Navbar.vue'
import Footer from '@/components/layout/Footer.vue'
import PasswordDialog from '@/components/nostr/PasswordDialog.vue'
import LoginDialog from '@/components/auth/LoginDialog.vue'
import { Toaster } from '@/components/ui/sonner'
import 'vue-sonner/style.css'
import { useNostr } from '@/composables/useNostr'
import { identity } from '@/composables/useIdentity'
import { auth } from '@/composables/useAuth'
import { toast } from 'vue-sonner'
import { useNostrStore } from '@/stores/nostr'
const { connect, disconnect } = useNostr()
const nostrStore = useNostrStore()
const showLoginDialog = ref(false)
const showPasswordDialog = ref(false)
async function handlePasswordUnlock(password: string) {
try {
await identity.loadWithPassword(password)
showPasswordDialog.value = false
toast.success('Identity unlocked successfully')
} catch (error) {
toast.error('Failed to unlock identity. Please check your password.')
}
}
function handlePasswordCancel() {
showPasswordDialog.value = false
function handleLoginSuccess() {
showLoginDialog.value = false
toast.success('Welcome back!')
}
onMounted(async () => {
// Check if we have an encrypted identity that needs password
if (identity.hasStoredIdentity() && identity.isStoredIdentityEncrypted()) {
showPasswordDialog.value = true
} else {
// Initialize identity system for non-encrypted identities
try {
await identity.initialize()
} catch (error) {
console.error('Failed to initialize identity:', error)
}
// Initialize authentication
try {
await auth.initialize()
} catch (error) {
console.error('Failed to initialize authentication:', error)
}
// Connect to Nostr relays
await connect()
// Check push notification status
await nostrStore.checkPushNotificationStatus()
})
onUnmounted(() => {
disconnect()
})
</script>
@ -64,6 +34,9 @@ onUnmounted(() => {
<nav
class="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 flex h-14 lg:h-16 xl:h-20 items-center justify-between">
<Navbar />
<<<<<<< HEAD <ConnectionStatus :is-connected="isConnected" :is-connecting="isConnecting" :error="error" />
=======
>>>>>>> 4686e5e (refactor: Transition to authentication system and remove identity management)
</nav>
</header>
@ -77,9 +50,13 @@ onUnmounted(() => {
<!-- Toast notifications -->
<Toaster />
<!-- Password unlock dialog -->
<PasswordDialog v-model:is-open="showPasswordDialog" title="Unlock Identity"
description="Your Nostr identity is encrypted. Enter your password to unlock it." @password="handlePasswordUnlock"
@cancel="handlePasswordCancel" />
<<<<<<< HEAD <!-- Password unlock dialog -->
<PasswordDialog v-model:is-open="showPasswordDialog" title="Unlock Identity"
description="Your Nostr identity is encrypted. Enter your password to unlock it."
@password="handlePasswordUnlock" @cancel="handlePasswordCancel" />
=======
<!-- Login dialog -->
<LoginDialog v-model:is-open="showLoginDialog" @success="handleLoginSuccess" />
>>>>>>> 4686e5e (refactor: Transition to authentication system and remove identity management)
</div>
</template>

View file

@ -0,0 +1,237 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { User } from 'lucide-vue-next'
import { auth } from '@/composables/useAuth'
import { toast } from 'vue-sonner'
interface Props {
isOpen: boolean
}
interface Emits {
(e: 'update:isOpen', value: boolean): void
(e: 'success'): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
// Form states
const activeTab = ref('login')
const isLoading = ref(false)
const error = ref('')
// Login form
const loginForm = ref({
username: '',
password: ''
})
// Register form
const registerForm = ref({
username: '',
email: '',
password: '',
password_repeat: ''
})
const canLogin = computed(() => {
return loginForm.value.username.trim() && loginForm.value.password.trim()
})
const canRegister = computed(() => {
const { username, password, password_repeat } = registerForm.value
return username.trim() && password.trim() && password === password_repeat && password.length >= 6
})
async function handleLogin() {
if (!canLogin.value) return
try {
isLoading.value = true
error.value = ''
await auth.login({
username: loginForm.value.username,
password: loginForm.value.password
})
toast.success('Login successful!')
emit('success')
handleClose()
} catch (err) {
error.value = err instanceof Error ? err.message : 'Login failed'
toast.error('Login failed. Please check your credentials.')
} finally {
isLoading.value = false
}
}
async function handleRegister() {
if (!canRegister.value) return
try {
isLoading.value = true
error.value = ''
await auth.register({
username: registerForm.value.username,
email: registerForm.value.email || undefined,
password: registerForm.value.password,
password_repeat: registerForm.value.password_repeat
})
toast.success('Registration successful!')
emit('success')
handleClose()
} catch (err) {
error.value = err instanceof Error ? err.message : 'Registration failed'
toast.error('Registration failed. Please try again.')
} finally {
isLoading.value = false
}
}
function handleClose() {
emit('update:isOpen', false)
// Reset forms
loginForm.value = { username: '', password: '' }
registerForm.value = { username: '', email: '', password: '', password_repeat: '' }
error.value = ''
isLoading.value = false
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
if (activeTab.value === 'login') {
handleLogin()
} else {
handleRegister()
}
}
}
</script>
<template>
<Dialog :open="isOpen" @update:open="handleClose">
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<User class="w-5 h-5" />
Authentication
</DialogTitle>
<DialogDescription>
Login to your account or create a new one
</DialogDescription>
</DialogHeader>
<Tabs v-model="activeTab" class="w-full">
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="login">Login</TabsTrigger>
<TabsTrigger value="register">Register</TabsTrigger>
</TabsList>
<TabsContent value="login" class="space-y-4">
<div class="space-y-4">
<div class="space-y-2">
<Label for="login-username">Username or Email</Label>
<Input
id="login-username"
v-model="loginForm.username"
placeholder="Enter your username or email"
:disabled="isLoading"
@keydown="handleKeydown"
/>
</div>
<div class="space-y-2">
<Label for="login-password">Password</Label>
<Input
id="login-password"
type="password"
v-model="loginForm.password"
placeholder="Enter your password"
:disabled="isLoading"
@keydown="handleKeydown"
/>
</div>
<p v-if="error && activeTab === 'login'" class="text-sm text-destructive">
{{ error }}
</p>
<Button
@click="handleLogin"
:disabled="isLoading || !canLogin"
class="w-full"
>
<span v-if="isLoading" class="animate-spin mr-2"></span>
Login
</Button>
</div>
</TabsContent>
<TabsContent value="register" class="space-y-4">
<div class="space-y-4">
<div class="space-y-2">
<Label for="register-username">Username</Label>
<Input
id="register-username"
v-model="registerForm.username"
placeholder="Choose a username"
:disabled="isLoading"
@keydown="handleKeydown"
/>
</div>
<div class="space-y-2">
<Label for="register-email">Email (optional)</Label>
<Input
id="register-email"
type="email"
v-model="registerForm.email"
placeholder="Enter your email"
:disabled="isLoading"
@keydown="handleKeydown"
/>
</div>
<div class="space-y-2">
<Label for="register-password">Password</Label>
<Input
id="register-password"
type="password"
v-model="registerForm.password"
placeholder="Choose a password"
:disabled="isLoading"
@keydown="handleKeydown"
/>
</div>
<div class="space-y-2">
<Label for="register-password-repeat">Confirm Password</Label>
<Input
id="register-password-repeat"
type="password"
v-model="registerForm.password_repeat"
placeholder="Confirm your password"
:disabled="isLoading"
@keydown="handleKeydown"
/>
</div>
<p v-if="error && activeTab === 'register'" class="text-sm text-destructive">
{{ error }}
</p>
<Button
@click="handleRegister"
:disabled="isLoading || !canRegister"
class="w-full"
>
<span v-if="isLoading" class="animate-spin mr-2"></span>
Register
</Button>
</div>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
</template>

View file

@ -0,0 +1,70 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { User, LogOut, Settings } from 'lucide-vue-next'
import { auth } from '@/composables/useAuth'
import { toast } from 'vue-sonner'
const userDisplay = computed(() => auth.userDisplay.value)
async function handleLogout() {
try {
await auth.logout()
toast.success('Logged out successfully')
} catch (error) {
toast.error('Failed to logout')
}
}
</script>
<template>
<div v-if="userDisplay" class="space-y-4">
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<User class="w-5 h-5" />
User Profile
</CardTitle>
<CardDescription>
Your account information
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="grid gap-4">
<div class="flex items-center justify-between">
<span class="text-sm font-medium">Name:</span>
<span class="text-sm">{{ userDisplay.name }}</span>
</div>
<div v-if="userDisplay.username" class="flex items-center justify-between">
<span class="text-sm font-medium">Username:</span>
<span class="text-sm">{{ userDisplay.username }}</span>
</div>
<div v-if="userDisplay.email" class="flex items-center justify-between">
<span class="text-sm font-medium">Email:</span>
<span class="text-sm">{{ userDisplay.email }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm font-medium">User ID:</span>
<Badge variant="secondary" class="text-xs">{{ userDisplay.shortId }}</Badge>
</div>
</div>
<div class="flex gap-2 pt-4">
<Button variant="outline" size="sm" class="flex-1">
<Settings class="w-4 h-4 mr-2" />
Settings
</Button>
<Button variant="destructive" size="sm" @click="handleLogout" class="flex-1">
<LogOut class="w-4 h-4 mr-2" />
Logout
</Button>
</div>
</CardContent>
</Card>
</div>
</template>

View file

@ -5,10 +5,9 @@ import { useTheme } from '@/components/theme-provider'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Sun, Moon, Menu, X, User, Key, LogOut } from 'lucide-vue-next'
import { Sun, Moon, Menu, X, User, LogOut } from 'lucide-vue-next'
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
import IdentityDialog from '@/components/nostr/IdentityDialog.vue'
import { identity } from '@/composables/useIdentity'
import { auth } from '@/composables/useAuth'
interface NavigationItem {
name: string
@ -18,7 +17,6 @@ interface NavigationItem {
const { t } = useI18n()
const { theme, setTheme } = useTheme()
const isOpen = ref(false)
const showIdentityDialog = ref(false)
const navigation = computed<NavigationItem[]>(() => [
{ name: t('nav.home'), href: '/' },
@ -34,11 +32,13 @@ const toggleTheme = () => {
setTheme(theme.value === 'dark' ? 'light' : 'dark')
}
const openIdentityDialog = () => {
console.log('openIdentityDialog called')
showIdentityDialog.value = true
// Close mobile menu when opening dialog
isOpen.value = false
const handleLogout = async () => {
try {
await auth.logout()
isOpen.value = false
} catch (error) {
console.error('Logout failed:', error)
}
}
</script>
@ -66,36 +66,32 @@ const openIdentityDialog = () => {
<!-- Theme Toggle, Language, and Identity -->
<div class="flex items-center gap-2">
<!-- Identity Management -->
<!-- Authentication Management -->
<div class="hidden sm:block">
<DropdownMenu v-if="identity.isAuthenticated.value">
<DropdownMenu v-if="auth.isAuthenticated.value">
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" class="gap-2">
<User class="h-4 w-4" />
<span class="max-w-24 truncate">{{ identity.profileDisplay.value?.name || 'Anonymous' }}</span>
<Badge variant="secondary" class="text-xs">Connected</Badge>
<span class="max-w-24 truncate">{{ auth.userDisplay.value?.name || 'Anonymous' }}</span>
<Badge variant="secondary" class="text-xs">Logged In</Badge>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-56">
<DropdownMenuItem @click="openIdentityDialog" class="gap-2">
<DropdownMenuItem class="gap-2">
<User class="h-4 w-4" />
Profile
</DropdownMenuItem>
<DropdownMenuItem @click="openIdentityDialog" class="gap-2">
<Key class="h-4 w-4" />
Keys
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem @click="identity.signOut()" class="gap-2 text-destructive">
<DropdownMenuItem @click="handleLogout" class="gap-2 text-destructive">
<LogOut class="h-4 w-4" />
Sign Out
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button v-else variant="outline" size="sm" @click="openIdentityDialog" class="gap-2">
<Button v-else variant="outline" size="sm" class="gap-2">
<User class="h-4 w-4" />
Connect Identity
Login
</Button>
</div>
@ -125,28 +121,28 @@ const openIdentityDialog = () => {
<div v-show="isOpen"
class="absolute left-0 right-0 top-14 z-40 border-b bg-background/95 backdrop-blur-sm md:hidden">
<div class="space-y-1 p-4">
<!-- Identity in mobile menu -->
<!-- Authentication in mobile menu -->
<div class="mb-4 px-2">
<Button v-if="!identity.isAuthenticated.value" variant="outline" size="sm" @click="openIdentityDialog"
<Button v-if="!auth.isAuthenticated.value" variant="outline" size="sm"
class="w-full gap-2 min-h-[44px] touch-manipulation">
<User class="h-4 w-4" />
Connect Identity
Login
</Button>
<div v-else class="space-y-2">
<div class="flex items-center gap-2 px-2 py-1">
<User class="h-4 w-4" />
<span class="text-sm font-medium">{{ identity.profileDisplay.value?.name || 'Anonymous' }}</span>
<Badge variant="secondary" class="text-xs ml-auto">Connected</Badge>
<span class="text-sm font-medium">{{ auth.userDisplay.value?.name || 'Anonymous' }}</span>
<Badge variant="secondary" class="text-xs ml-auto">Logged In</Badge>
</div>
<div class="space-y-1">
<Button variant="ghost" size="sm" @click="openIdentityDialog" class="w-full justify-start gap-2">
<Button variant="ghost" size="sm" class="w-full justify-start gap-2">
<User class="h-4 w-4" />
Profile
</Button>
<Button variant="ghost" size="sm" @click="identity.signOut(); isOpen = false"
<Button variant="ghost" size="sm" @click="handleLogout"
class="w-full justify-start gap-2 text-destructive">
<LogOut class="h-4 w-4" />
Sign Out
Logout
</Button>
</div>
</div>
@ -172,7 +168,5 @@ const openIdentityDialog = () => {
</div>
</div>
<!-- Identity Dialog -->
<IdentityDialog v-model:is-open="showIdentityDialog" />
</nav>
</template>

166
src/composables/useAuth.ts Normal file
View file

@ -0,0 +1,166 @@
import { ref, computed } from 'vue'
import { lnbitsAPI, type User, type LoginCredentials, type RegisterData } from '@/lib/api/lnbits'
const currentUser = ref<User | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
export function useAuth() {
const isAuthenticated = computed(() => !!currentUser.value)
/**
* Initialize authentication on app start
*/
async function initialize(): Promise<void> {
try {
isLoading.value = true
error.value = null
if (lnbitsAPI.isAuthenticated()) {
const user = await lnbitsAPI.getCurrentUser()
currentUser.value = user
}
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to initialize authentication'
// Clear invalid token
await logout()
} finally {
isLoading.value = false
}
}
/**
* Login with username and password
*/
async function login(credentials: LoginCredentials): Promise<void> {
try {
isLoading.value = true
error.value = null
await lnbitsAPI.login(credentials)
// Get user details
const user = await lnbitsAPI.getCurrentUser()
currentUser.value = user
} catch (err) {
error.value = err instanceof Error ? err.message : 'Login failed'
throw err
} finally {
isLoading.value = false
}
}
/**
* Register new user
*/
async function register(data: RegisterData): Promise<void> {
try {
isLoading.value = true
error.value = null
await lnbitsAPI.register(data)
// Get user details
const user = await lnbitsAPI.getCurrentUser()
currentUser.value = user
} catch (err) {
error.value = err instanceof Error ? err.message : 'Registration failed'
throw err
} finally {
isLoading.value = false
}
}
/**
* Logout and clear user data
*/
async function logout(): Promise<void> {
try {
await lnbitsAPI.logout()
} catch (err) {
console.error('Logout error:', err)
} finally {
currentUser.value = null
error.value = null
}
}
/**
* Update user password
*/
async function updatePassword(currentPassword: string, newPassword: string): Promise<void> {
try {
isLoading.value = true
error.value = null
const updatedUser = await lnbitsAPI.updatePassword(currentPassword, newPassword)
currentUser.value = updatedUser
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to update password'
throw err
} finally {
isLoading.value = false
}
}
/**
* Update user profile
*/
async function updateProfile(data: Partial<User>): Promise<void> {
try {
isLoading.value = true
error.value = null
const updatedUser = await lnbitsAPI.updateProfile(data)
currentUser.value = updatedUser
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to update profile'
throw err
} finally {
isLoading.value = false
}
}
/**
* Check if user is authenticated
*/
function checkAuth(): boolean {
return lnbitsAPI.isAuthenticated()
}
/**
* Get user display info
*/
const userDisplay = computed(() => {
if (!currentUser.value) return null
return {
name: currentUser.value.username || currentUser.value.email || 'Anonymous',
username: currentUser.value.username,
email: currentUser.value.email,
id: currentUser.value.id,
shortId: currentUser.value.id.slice(0, 8) + '...' + currentUser.value.id.slice(-8)
}
})
return {
// State
currentUser: computed(() => currentUser.value),
isAuthenticated,
isLoading,
error,
userDisplay,
// Actions
initialize,
login,
register,
logout,
updatePassword,
updateProfile,
checkAuth
}
}
// Export singleton instance for global state
export const auth = useAuth()

132
src/lib/api/lnbits.ts Normal file
View file

@ -0,0 +1,132 @@
interface LoginCredentials {
username: string
password: string
}
interface RegisterData {
username: string
email?: string
password: string
password_repeat: string
}
interface AuthResponse {
access_token: string
user_id: string
username?: string
email?: string
}
interface User {
id: string
username?: string
email?: string
pubkey?: string
created_at: string
updated_at: string
}
import { getApiUrl, getAuthToken, setAuthToken, removeAuthToken } from '@/lib/config/lnbits'
class LnbitsAPI {
private accessToken: string | null = null
constructor() {
// Try to load token from localStorage
this.accessToken = getAuthToken()
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = getApiUrl(endpoint)
const headers: HeadersInit = {
'Content-Type': 'application/json',
...options.headers,
}
if (this.accessToken) {
(headers as Record<string, string>)['Authorization'] = `Bearer ${this.accessToken}`
}
const response = await fetch(url, {
...options,
headers,
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
async login(credentials: LoginCredentials): Promise<AuthResponse> {
const response = await this.request<AuthResponse>('/auth', {
method: 'POST',
body: JSON.stringify(credentials),
})
this.accessToken = response.access_token
setAuthToken(response.access_token)
return response
}
async register(data: RegisterData): Promise<AuthResponse> {
const response = await this.request<AuthResponse>('/auth/register', {
method: 'POST',
body: JSON.stringify(data),
})
this.accessToken = response.access_token
setAuthToken(response.access_token)
return response
}
async logout(): Promise<void> {
try {
await this.request('/auth/logout', {
method: 'POST',
})
} finally {
this.accessToken = null
removeAuthToken()
}
}
async getCurrentUser(): Promise<User> {
return this.request<User>('/auth')
}
async updatePassword(currentPassword: string, newPassword: string): Promise<User> {
return this.request<User>('/auth/password', {
method: 'PUT',
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword,
}),
})
}
async updateProfile(data: Partial<User>): Promise<User> {
return this.request<User>('/auth/update', {
method: 'PUT',
body: JSON.stringify(data),
})
}
isAuthenticated(): boolean {
return !!this.accessToken
}
getAccessToken(): string | null {
return this.accessToken
}
}
export const lnbitsAPI = new LnbitsAPI()
export type { LoginCredentials, RegisterData, AuthResponse, User }

38
src/lib/config/lnbits.ts Normal file
View file

@ -0,0 +1,38 @@
// LNBits API Configuration
export const LNBITS_CONFIG = {
// Base URL for the LNBits API
// This should point to your LNBits instance
API_BASE_URL: import.meta.env.VITE_LNBITS_API_URL || '/api/v1',
// Whether to enable debug logging
DEBUG: import.meta.env.VITE_LNBITS_DEBUG === 'true',
// Timeout for API requests (in milliseconds)
REQUEST_TIMEOUT: 10000,
// Auth token storage key
AUTH_TOKEN_KEY: 'lnbits_access_token',
// User storage key
USER_STORAGE_KEY: 'lnbits_user_data'
}
// Helper function to get the full API URL
export function getApiUrl(endpoint: string): string {
return `${LNBITS_CONFIG.API_BASE_URL}${endpoint}`
}
// Helper function to get auth token from storage
export function getAuthToken(): string | null {
return localStorage.getItem(LNBITS_CONFIG.AUTH_TOKEN_KEY)
}
// Helper function to set auth token in storage
export function setAuthToken(token: string): void {
localStorage.setItem(LNBITS_CONFIG.AUTH_TOKEN_KEY, token)
}
// Helper function to remove auth token from storage
export function removeAuthToken(): void {
localStorage.removeItem(LNBITS_CONFIG.AUTH_TOKEN_KEY)
}

View file

@ -2,26 +2,23 @@
<div class="container py-8 space-y-6">
<PWAInstallPrompt auto-show />
<NotificationPermission auto-show />
<NostrFeed feed-type="announcements" />
<!-- Welcome Section -->
<div class="text-center space-y-4">
<h1 class="text-3xl font-bold tracking-tight">Welcome back!</h1>
<p class="text-lg text-muted-foreground">
You are logged in as {{ auth.userDisplay.value?.name }}
</p>
</div>
<!-- User Profile Card -->
<UserProfile />
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import NostrFeed from '@/components/nostr/NostrFeed.vue'
import NotificationPermission from '@/components/notifications/NotificationPermission.vue'
import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue'
import { useNostrFeedPreloader } from '@/composables/useNostrFeedPreloader'
// Preload the announcements feed for better UX
const feedPreloader = useNostrFeedPreloader({
feedType: 'announcements',
limit: 50,
includeReplies: false
})
onMounted(async () => {
// Auto-preload if we have stored data
await feedPreloader.autoPreload()
})
import UserProfile from '@/components/auth/UserProfile.vue'
import { auth } from '@/composables/useAuth'
</script>

217
src/pages/Login.vue Normal file
View file

@ -0,0 +1,217 @@
<template>
<div class="container py-8 space-y-6">
<div class="flex flex-col items-center justify-center min-h-[60vh] space-y-8">
<!-- Welcome Section -->
<div class="text-center space-y-4">
<div class="flex justify-center">
<img src="@/assets/logo-72px.png" alt="Logo" class="h-24 w-24" />
</div>
<h1 class="text-4xl font-bold tracking-tight">Welcome to Ario</h1>
<p class="text-xl text-muted-foreground max-w-md">
Your secure platform for events and community management
</p>
</div>
<!-- Login Card -->
<Card class="w-full max-w-md">
<CardHeader>
<CardTitle class="text-center">Sign In</CardTitle>
<CardDescription class="text-center">
Enter your credentials to access your account
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="space-y-2">
<Label for="username">Username or Email</Label>
<Input
id="username"
v-model="loginForm.username"
placeholder="Enter your username or email"
@keydown.enter="handleLogin"
/>
</div>
<div class="space-y-2">
<Label for="password">Password</Label>
<Input
id="password"
type="password"
v-model="loginForm.password"
placeholder="Enter your password"
@keydown.enter="handleLogin"
/>
</div>
<p v-if="error" class="text-sm text-destructive text-center">
{{ error }}
</p>
<Button
@click="handleLogin"
:disabled="isLoading || !canLogin"
class="w-full"
>
<span v-if="isLoading" class="animate-spin mr-2"></span>
Sign In
</Button>
<div class="text-center">
<p class="text-sm text-muted-foreground">
Don't have an account?
<Button variant="link" @click="showRegister = true" class="p-0 h-auto">
Create one
</Button>
</p>
</div>
</CardContent>
</Card>
<!-- Register Card -->
<Card v-if="showRegister" class="w-full max-w-md">
<CardHeader>
<CardTitle class="text-center">Create Account</CardTitle>
<CardDescription class="text-center">
Create a new account to get started
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="space-y-2">
<Label for="reg-username">Username</Label>
<Input
id="reg-username"
v-model="registerForm.username"
placeholder="Choose a username"
@keydown.enter="handleRegister"
/>
</div>
<div class="space-y-2">
<Label for="reg-email">Email (optional)</Label>
<Input
id="reg-email"
type="email"
v-model="registerForm.email"
placeholder="Enter your email"
@keydown.enter="handleRegister"
/>
</div>
<div class="space-y-2">
<Label for="reg-password">Password</Label>
<Input
id="reg-password"
type="password"
v-model="registerForm.password"
placeholder="Choose a password"
@keydown.enter="handleRegister"
/>
</div>
<div class="space-y-2">
<Label for="reg-password-repeat">Confirm Password</Label>
<Input
id="reg-password-repeat"
type="password"
v-model="registerForm.password_repeat"
placeholder="Confirm your password"
@keydown.enter="handleRegister"
/>
</div>
<p v-if="error" class="text-sm text-destructive text-center">
{{ error }}
</p>
<Button
@click="handleRegister"
:disabled="isLoading || !canRegister"
class="w-full"
>
<span v-if="isLoading" class="animate-spin mr-2"></span>
Create Account
</Button>
<div class="text-center">
<p class="text-sm text-muted-foreground">
Already have an account?
<Button variant="link" @click="showRegister = false" class="p-0 h-auto">
Sign in
</Button>
</p>
</div>
</CardContent>
</Card>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { auth } from '@/composables/useAuth'
import { toast } from 'vue-sonner'
const showRegister = ref(false)
const isLoading = ref(false)
const error = ref('')
// Login form
const loginForm = ref({
username: '',
password: ''
})
// Register form
const registerForm = ref({
username: '',
email: '',
password: '',
password_repeat: ''
})
const canLogin = computed(() => {
return loginForm.value.username.trim() && loginForm.value.password.trim()
})
const canRegister = computed(() => {
const { username, password, password_repeat } = registerForm.value
return username.trim() && password.trim() && password === password_repeat && password.length >= 6
})
async function handleLogin() {
if (!canLogin.value) return
try {
isLoading.value = true
error.value = ''
await auth.login({
username: loginForm.value.username,
password: loginForm.value.password
})
toast.success('Login successful!')
} catch (err) {
error.value = err instanceof Error ? err.message : 'Login failed'
toast.error('Login failed. Please check your credentials.')
} finally {
isLoading.value = false
}
}
async function handleRegister() {
if (!canRegister.value) return
try {
isLoading.value = true
error.value = ''
await auth.register({
username: registerForm.value.username,
email: registerForm.value.email || undefined,
password: registerForm.value.password,
password_repeat: registerForm.value.password_repeat
})
toast.success('Registration successful!')
} catch (err) {
error.value = err instanceof Error ? err.message : 'Registration failed'
toast.error('Registration failed. Please try again.')
} finally {
isLoading.value = false
}
}
</script>

View file

@ -1,5 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import { auth } from '@/composables/useAuth'
import Home from '@/pages/Home.vue'
import Login from '@/pages/Login.vue'
const router = createRouter({
history: createWebHistory(),
@ -7,17 +9,47 @@ const router = createRouter({
{
path: '/',
name: 'home',
component: Home
component: Home,
meta: {
requiresAuth: true
}
},
{
path: '/login',
name: 'login',
component: Login,
meta: {
requiresAuth: false
}
},
{
path: '/events',
name: 'events',
component: () => import('@/pages/events.vue'),
meta: {
title: 'Events'
title: 'Events',
requiresAuth: true
}
}
]
})
// Navigation guard to check authentication
router.beforeEach(async (to, _from, next) => {
// Initialize auth if not already done
if (!auth.isAuthenticated.value && auth.checkAuth()) {
await auth.initialize()
}
if (to.meta.requiresAuth && !auth.isAuthenticated.value) {
// Redirect to login if authentication is required but user is not authenticated
next('/login')
} else if (to.path === '/login' && auth.isAuthenticated.value) {
// Redirect to home if user is already authenticated and trying to access login
next('/')
} else {
next()
}
})
export default router