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

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:

  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

<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

  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)