# Quickstart: Basic Login Protection **Feature**: 003-basic-login **Date**: 2026-04-06 ## What Changes | Component | Change | |-----------|--------| | `SecurityConfig.java` | Username made configurable via `app.auth.username` property | | `AuthController.java` | New: `GET /api/v1/auth/check` endpoint | | `authStore.ts` | New: Pinia store managing credentials + sessionStorage | | `LoginView.vue` | New: login form page | | `api.ts` | Replace hardcoded Basic Auth with dynamic interceptor | | `router/index.ts` | Add `/login` route + `beforeEach` navigation guard | | `App.vue` | Add logout button to navbar | | `application.yaml` | Add `app.auth.username` property with default | ## Backend Setup ### 1. Add username to application.yaml ```yaml app: auth: username: ${APP_AUTH_USERNAME:neurosurgeon} password: ${APP_AUTH_PASSWORD} # already present ``` ### 2. Update SecurityConfig.java Inject both username and password: ```java @Bean public UserDetailsService userDetailsService( @Value("${app.auth.username}") String username, @Value("${app.auth.password}") String password) { UserDetails user = User.builder() .username(username) .password("{noop}" + password) .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } ``` ### 3. Add AuthController.java ```java @RestController @RequestMapping("/api/v1/auth") public class AuthController { @GetMapping("/check") public ResponseEntity> check(Principal principal) { return ResponseEntity.ok(Map.of("username", principal.getName())); } } ``` ## Frontend Setup ### 1. Create authStore.ts ```typescript // src/stores/authStore.ts import { defineStore } from 'pinia' import { ref, computed } from 'vue' const SESSION_KEY = 'auth' export const useAuthStore = defineStore('auth', () => { const stored = sessionStorage.getItem(SESSION_KEY) const parsed = stored ? JSON.parse(stored) : null const username = ref(parsed?.username ?? null) const password = ref(parsed?.password ?? null) const isAuthenticated = computed(() => !!username.value && !!password.value) function setCredentials(u: string, p: string) { username.value = u password.value = p sessionStorage.setItem(SESSION_KEY, JSON.stringify({ username: u, password: p })) } function clearCredentials() { username.value = null password.value = null sessionStorage.removeItem(SESSION_KEY) } return { username, password, isAuthenticated, setCredentials, clearCredentials } }) ``` ### 2. Update api.ts Replace hardcoded `auth` with a request interceptor: ```typescript import axios from 'axios' import { useAuthStore } from '@/stores/authStore' export const api = axios.create({ baseURL: import.meta.env.VITE_API_URL ?? '/api/v1', headers: { 'Content-Type': 'application/json' } }) api.interceptors.request.use((config) => { const auth = useAuthStore() if (auth.username && auth.password) { config.auth = { username: auth.username, password: auth.password } } return config }) api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { useAuthStore().clearCredentials() window.location.href = '/login' } const message = error.response?.data?.error ?? error.message ?? 'An unexpected error occurred.' return Promise.reject(new Error(message)) } ) ``` ### 3. Update router/index.ts Add `/login` route and guard: ```typescript import LoginView from '@/views/LoginView.vue' import { useAuthStore } from '@/stores/authStore' // add to routes array: { path: '/login', name: 'login', component: LoginView } // add global guard: router.beforeEach((to) => { const auth = useAuthStore() if (to.name !== 'login' && !auth.isAuthenticated) { return { name: 'login' } } }) ``` ### 4. Create LoginView.vue A simple centered form with username and password fields. On submit: 1. Store credentials tentatively in the auth store 2. Call `GET /api/v1/auth/check` 3. If 200 → navigate to `/` 4. If 401 → clear credentials, show error message ### 5. Add logout to App.vue navbar ```html ``` ```typescript import { useAuthStore } from '@/stores/authStore' import { useRouter } from 'vue-router' const auth = useAuthStore() const router = useRouter() function logout() { auth.clearCredentials() router.push({ name: 'login' }) } ``` ## Environment Variables ### Backend (.env / docker-compose environment) ``` APP_AUTH_USERNAME=neurosurgeon # optional, defaults to neurosurgeon APP_AUTH_PASSWORD=your-secret ``` ### Frontend (.env) ``` VITE_API_URL=/api/v1 # VITE_APP_PASSWORD is no longer needed and should be removed ``` ## Testing the Login Flow 1. Open the app in an incognito window — should redirect to `/login` 2. Enter wrong credentials → error message, stay on login 3. Enter correct credentials → redirect to `/` (Library) 4. Refresh the page → stay logged in 5. Click "Sign out" → redirect to `/login`; back button shows login again (no cached page access)