5.1 KiB
5.1 KiB
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
app:
auth:
username: ${APP_AUTH_USERNAME:neurosurgeon}
password: ${APP_AUTH_PASSWORD} # already present
2. Update SecurityConfig.java
Inject both username and password:
@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
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@GetMapping("/check")
public ResponseEntity<Map<String, String>> check(Principal principal) {
return ResponseEntity.ok(Map.of("username", principal.getName()));
}
}
Frontend Setup
1. Create authStore.ts
// 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<string | null>(parsed?.username ?? null)
const password = ref<string | null>(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:
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:
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:
- Store credentials tentatively in the auth store
- Call
GET /api/v1/auth/check - If 200 → navigate to
/ - If 401 → clear credentials, show error message
5. Add logout to App.vue navbar
<button class="btn btn-secondary" @click="logout">Sign out</button>
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
- Open the app in an incognito window — should redirect to
/login - Enter wrong credentials → error message, stay on login
- Enter correct credentials → redirect to
/(Library) - Refresh the page → stay logged in
- Click "Sign out" → redirect to
/login; back button shows login again (no cached page access)