Files
ai-teacher/specs/003-basic-login/quickstart.md
T
2026-04-06 14:29:53 +02:00

199 lines
5.1 KiB
Markdown

# 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<Map<String, String>> 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<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:
```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
<button class="btn btn-secondary" @click="logout">Sign out</button>
```
```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)