Added auth stuff to the web app.

This commit is contained in:
Andrew Lalis 2023-01-30 11:55:09 +01:00
parent 0e96d74203
commit f8dcfc8843
8 changed files with 116 additions and 21 deletions

View File

@ -2,6 +2,7 @@ package nl.andrewlalis.gymboard_api.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@ -9,6 +10,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@ -20,17 +24,44 @@ public class SecurityConfig {
this.tokenAuthenticationFilter = tokenAuthenticationFilter; this.tokenAuthenticationFilter = tokenAuthenticationFilter;
} }
/**
* Defines the security configuration we'll use for this API.
* @param http The security configurable.
* @return The filter chain to apply.
* @throws Exception If an error occurs while configuring.
*/
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http http
.httpBasic().disable() .httpBasic().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.csrf().disable() .csrf().disable()
.cors().and()
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests().anyRequest().permitAll(); .authorizeHttpRequests().anyRequest().permitAll();
return http.build(); return http.build();
} }
/**
* Defines the CORS configuration for this API, which is to say that we
* allow cross-origin requests ONLY from the web app for the vast majority
* of endpoints.
* @return The CORS configuration source.
*/
@Bean
@Order(1)
public CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// Don't do this in production, use a proper list of allowed origins
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return source;
}
@Bean @Bean
public AuthenticationManager authenticationManager() { public AuthenticationManager authenticationManager() {
return null;// Disable the standard spring authentication manager. return null;// Disable the standard spring authentication manager.

View File

@ -1,31 +1,11 @@
package nl.andrewlalis.gymboard_api.config; package nl.andrewlalis.gymboard_api.config;
import nl.andrewlalis.gymboard_api.util.ULID; import nl.andrewlalis.gymboard_api.util.ULID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.time.OffsetDateTime;
import java.util.Arrays;
@Configuration @Configuration
public class WebConfig { public class WebConfig {
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// Don't do this in production, use a proper list of allowed origins
config.addAllowedOriginPattern("*");
config.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH"));
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Bean @Bean
public ULID ulid() { public ULID ulid() {
return new ULID(); return new ULID();

View File

@ -2,7 +2,12 @@ package nl.andrewlalis.gymboard_api.controller;
import nl.andrewlalis.gymboard_api.controller.dto.TokenCredentials; import nl.andrewlalis.gymboard_api.controller.dto.TokenCredentials;
import nl.andrewlalis.gymboard_api.controller.dto.TokenResponse; import nl.andrewlalis.gymboard_api.controller.dto.TokenResponse;
import nl.andrewlalis.gymboard_api.controller.dto.UserResponse;
import nl.andrewlalis.gymboard_api.model.auth.User;
import nl.andrewlalis.gymboard_api.service.auth.TokenService; import nl.andrewlalis.gymboard_api.service.auth.TokenService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -19,4 +24,10 @@ public class AuthController {
public TokenResponse getToken(@RequestBody TokenCredentials credentials) { public TokenResponse getToken(@RequestBody TokenCredentials credentials) {
return tokenService.generateAccessToken(credentials); return tokenService.generateAccessToken(credentials);
} }
@GetMapping(path = "/auth/me")
@PreAuthorize("isAuthenticated()")
public UserResponse getMyUser(@AuthenticationPrincipal User user) {
return new UserResponse(user);
}
} }

View File

@ -0,0 +1,30 @@
import { api } from 'src/api/main/index';
export interface User {
id: string;
activated: boolean;
email: string;
name: string;
}
export interface TokenCredentials {
email: string;
password: string;
}
class AuthModule {
public async getToken(credentials: TokenCredentials): Promise<string> {
const response = await api.post('/auth/token', credentials);
return response.data.token;
}
public async getMyUser(token: string): Promise<User> {
const response = await api.get('/auth/me', {
headers: {
'Authorization': 'Bearer ' + token
}
});
return response.data;
}
}
export default AuthModule;

View File

@ -2,6 +2,7 @@ import axios from 'axios';
import GymsModule from 'src/api/main/gyms'; import GymsModule from 'src/api/main/gyms';
import ExercisesModule from 'src/api/main/exercises'; import ExercisesModule from 'src/api/main/exercises';
import LeaderboardsModule from 'src/api/main/leaderboards'; import LeaderboardsModule from 'src/api/main/leaderboards';
import AuthModule from 'src/api/main/auth';
export const BASE_URL = 'http://localhost:8080'; export const BASE_URL = 'http://localhost:8080';
@ -11,6 +12,7 @@ export const api = axios.create({
}); });
class GymboardApi { class GymboardApi {
public readonly auth = new AuthModule();
public readonly gyms = new GymsModule(); public readonly gyms = new GymsModule();
public readonly exercises = new ExercisesModule(); public readonly exercises = new ExercisesModule();
public readonly leaderboards = new LeaderboardsModule(); public readonly leaderboards = new LeaderboardsModule();

View File

@ -41,8 +41,9 @@
<q-item-label header> <q-item-label header>
{{ $t('mainLayout.pages') }} {{ $t('mainLayout.pages') }}
</q-item-label> </q-item-label>
<q-item clickable>Gyms</q-item> <q-item clickable to="/">Gyms</q-item>
<q-item clickable>Global Leaderboard</q-item> <q-item clickable>Global Leaderboard</q-item>
<q-item clickable to="/testing">Testing Page</q-item>
</q-list> </q-list>
</q-drawer> </q-drawer>

View File

@ -0,0 +1,38 @@
<template>
<StandardCenteredPage>
<h3>Testing Page</h3>
<p>
Use this page to test new functionality, before adding it to the main
app. This page should be hidden on production.
</p>
<div class="row" style="border: 3px solid red">
<h4>Auth Test</h4>
<q-btn
label="Do auth"
@click="doAuth()"
/>
<p>{{ authTestMessage }}</p>
</div>
</StandardCenteredPage>
</template>
<script setup lang="ts">
import StandardCenteredPage from 'src/components/StandardCenteredPage.vue';
import api from 'src/api/main';
import {ref} from 'vue';
import {sleep} from "src/utils";
const authTestMessage = ref('');
async function doAuth() {
const token = await api.auth.getToken({email: 'andrew.lalis@example.com', password: 'testpass'});
authTestMessage.value = 'Token: ' + token;
await sleep(2000);
const user = await api.auth.getMyUser(token);
authTestMessage.value = 'User: ' + JSON.stringify(user);
}
</script>
<style scoped>
</style>

View File

@ -5,6 +5,7 @@ import GymPage from 'pages/gym/GymPage.vue';
import GymSubmissionPage from 'pages/gym/GymSubmissionPage.vue'; import GymSubmissionPage from 'pages/gym/GymSubmissionPage.vue';
import GymHomePage from 'pages/gym/GymHomePage.vue'; import GymHomePage from 'pages/gym/GymHomePage.vue';
import GymLeaderboardsPage from 'pages/gym/GymLeaderboardsPage.vue'; import GymLeaderboardsPage from 'pages/gym/GymLeaderboardsPage.vue';
import TestingPage from 'pages/TestingPage.vue';
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
@ -12,6 +13,7 @@ const routes: RouteRecordRaw[] = [
component: MainLayout, component: MainLayout,
children: [ children: [
{ path: '', component: IndexPage }, { path: '', component: IndexPage },
{ path: 'testing', component: TestingPage },
{ {
path: 'gyms/:countryCode/:cityShortName/:gymShortName', path: 'gyms/:countryCode/:cityShortName/:gymShortName',
component: GymPage, component: GymPage,