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:
parent
5ceb12ca3b
commit
be4ab13b32
11 changed files with 1065 additions and 96 deletions
109
AUTHENTICATION.md
Normal file
109
AUTHENTICATION.md
Normal 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.
|
||||
69
src/App.vue
69
src/App.vue
|
|
@ -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>
|
||||
|
|
|
|||
237
src/components/auth/LoginDialog.vue
Normal file
237
src/components/auth/LoginDialog.vue
Normal 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>
|
||||
70
src/components/auth/UserProfile.vue
Normal file
70
src/components/auth/UserProfile.vue
Normal 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>
|
||||
|
|
@ -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
166
src/composables/useAuth.ts
Normal 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
132
src/lib/api/lnbits.ts
Normal 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
38
src/lib/config/lnbits.ts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
217
src/pages/Login.vue
Normal 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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue