diff --git a/AUTHENTICATION.md b/AUTHENTICATION.md new file mode 100644 index 0000000..ed8e0ad --- /dev/null +++ b/AUTHENTICATION.md @@ -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. \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 440d443..56cd297 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,57 +1,27 @@ @@ -64,6 +34,9 @@ onUnmounted(() => { @@ -77,9 +50,13 @@ onUnmounted(() => { - - + <<<<<<< HEAD + + ======= + + + >>>>>>> 4686e5e (refactor: Transition to authentication system and remove identity management) diff --git a/src/components/auth/LoginDialog.vue b/src/components/auth/LoginDialog.vue new file mode 100644 index 0000000..eca7954 --- /dev/null +++ b/src/components/auth/LoginDialog.vue @@ -0,0 +1,237 @@ + + + \ No newline at end of file diff --git a/src/components/auth/UserProfile.vue b/src/components/auth/UserProfile.vue new file mode 100644 index 0000000..7982f5a --- /dev/null +++ b/src/components/auth/UserProfile.vue @@ -0,0 +1,70 @@ + + + \ No newline at end of file diff --git a/src/components/layout/Navbar.vue b/src/components/layout/Navbar.vue index db7cdab..59123b8 100644 --- a/src/components/layout/Navbar.vue +++ b/src/components/layout/Navbar.vue @@ -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(() => [ { 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) + } } @@ -66,36 +66,32 @@ const openIdentityDialog = () => {
- + @@ -125,28 +121,28 @@ const openIdentityDialog = () => {
- +
-
- {{ identity.profileDisplay.value?.name || 'Anonymous' }} - Connected + {{ auth.userDisplay.value?.name || 'Anonymous' }} + Logged In
- -
@@ -172,7 +168,5 @@ const openIdentityDialog = () => {
- - diff --git a/src/composables/useAuth.ts b/src/composables/useAuth.ts new file mode 100644 index 0000000..fa8eca4 --- /dev/null +++ b/src/composables/useAuth.ts @@ -0,0 +1,166 @@ +import { ref, computed } from 'vue' +import { lnbitsAPI, type User, type LoginCredentials, type RegisterData } from '@/lib/api/lnbits' + +const currentUser = ref(null) +const isLoading = ref(false) +const error = ref(null) + +export function useAuth() { + const isAuthenticated = computed(() => !!currentUser.value) + + /** + * Initialize authentication on app start + */ + async function initialize(): Promise { + 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 { + 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 { + 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 { + 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 { + 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): Promise { + 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() \ No newline at end of file diff --git a/src/lib/api/lnbits.ts b/src/lib/api/lnbits.ts new file mode 100644 index 0000000..5bb8164 --- /dev/null +++ b/src/lib/api/lnbits.ts @@ -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( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = getApiUrl(endpoint) + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...options.headers, + } + + if (this.accessToken) { + (headers as Record)['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 { + const response = await this.request('/auth', { + method: 'POST', + body: JSON.stringify(credentials), + }) + + this.accessToken = response.access_token + setAuthToken(response.access_token) + + return response + } + + async register(data: RegisterData): Promise { + const response = await this.request('/auth/register', { + method: 'POST', + body: JSON.stringify(data), + }) + + this.accessToken = response.access_token + setAuthToken(response.access_token) + + return response + } + + async logout(): Promise { + try { + await this.request('/auth/logout', { + method: 'POST', + }) + } finally { + this.accessToken = null + removeAuthToken() + } + } + + async getCurrentUser(): Promise { + return this.request('/auth') + } + + async updatePassword(currentPassword: string, newPassword: string): Promise { + return this.request('/auth/password', { + method: 'PUT', + body: JSON.stringify({ + current_password: currentPassword, + new_password: newPassword, + }), + }) + } + + async updateProfile(data: Partial): Promise { + return this.request('/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 } \ No newline at end of file diff --git a/src/lib/config/lnbits.ts b/src/lib/config/lnbits.ts new file mode 100644 index 0000000..74a5ead --- /dev/null +++ b/src/lib/config/lnbits.ts @@ -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) +} \ No newline at end of file diff --git a/src/pages/Home.vue b/src/pages/Home.vue index 9644bba..ee10443 100644 --- a/src/pages/Home.vue +++ b/src/pages/Home.vue @@ -2,26 +2,23 @@
- + + +
+

Welcome back!

+

+ You are logged in as {{ auth.userDisplay.value?.name }} +

+
+ + +
diff --git a/src/pages/Login.vue b/src/pages/Login.vue new file mode 100644 index 0000000..a0369cc --- /dev/null +++ b/src/pages/Login.vue @@ -0,0 +1,217 @@ + + + \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index 6aafe87..b08cdaa 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -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