Skip to content
Snippets Groups Projects
Commit 918999e0 authored by Elisabethein's avatar Elisabethein
Browse files

Fixed some authentication bugs. Added alert for token expiration. #86 #85

parent 62e06df6
No related branches found
No related tags found
No related merge requests found
......@@ -73,10 +73,32 @@ This project is owned by the Estonian Wildlife Center and is not open source.
### Documentation
#### Description of the general workflow:
1. The user opens the application and is on a welcome page that describes the organization.
What the user can do without logging in:
2. The user can navigate to the alert page to report an injured animal.
3. The user can navigate to the registration page to create an application to become a volunteer.
4. The user can navigate to the login page and insert their email and password to log in.
What happens when user logs in:
5. The server create a token for them, which they can use to access the main application and its features.
6. The token will expire after 1 hour.
7. If the token is expired the frontend should redirect the user to the login page. This can be done by checking the backend's response and handling un-authorization with logging out.
8. If the un-authorization is not handled, the user will be stuck in the application without the ability to access any features and probably error messages.
9. If the token is not expired the user should be logged in after refreshing the page and even when closing the application and reopening within an hour.
What the user can do after logging in:
10. The user can navigate between the different pages of the application defined in the navigation bar.
11. The user can go to their own profile.
12. The user can log out.
#### Backend
##### Entities
These are the entities used in the application. They are used to represent the database tables.
* Users are connected to roles, regions, species and tags. Tickets are connected to users, species and posts
* Users are connected to roles, regions, species and tags.
* Tickets are connected to users, species and posts
* Applications are connected to tags.
* Species are connected to upper species and tickets.
......
......@@ -214,6 +214,8 @@ export default {
const updatedUser = await response.json();
this.$emit('close-popup');
this.$emit('update-user', updatedUser);
} else if (response.status===403) {
this.handleUnauthorized(response);
}
} catch (error) {
console.error(error);
......@@ -237,6 +239,8 @@ export default {
if (response.ok) {
this.$emit('close-popup');
this.$emit('delete-user', this.editableUser);
} else if (response.status===403) {
this.handleUnauthorized(response);
}
} catch (error) {
console.error(error);
......@@ -244,6 +248,12 @@ export default {
}
}
},
handleUnauthorized(response) {
console.error('Unauthorized:', response.statusText);
alert('Teie sessioon on aegunud, palun logige uuesti sisse!');
this.$store.dispatch('logout');
this.$router.push('/login');
},
},
};
</script>
......
......@@ -110,8 +110,8 @@
<!-- Experience field -->
<div class="form-group">
<label for="experience">Kas kuulud mõnda loomadega tegelevasse organisatsiooni? Kui jah, siis mis
organisatsiooni?*</label>
<textarea v-model="formData.experience" id="experience" required placeholder="Kirjuta siia..."></textarea>
organisatsiooni?</label>
<textarea v-model="formData.experience" id="experience" placeholder="Kirjuta siia..."></textarea>
</div>
<!-- Password field -->
......@@ -172,10 +172,7 @@ export default {
shareData: false,
allowControlVisits: false
},
regions: [
"Harjumaa", "Hiiumaa", "Ida-Virumaa", "Jõgevamaa", "Järvamaa", "Läänemaa", "Lääne-Virumaa",
"Põlvamaa", "Pärnumaa", "Raplamaa", "Saaremaa", "Tartumaa", "Valgamaa", "Viljandimaa", "Võrumaa"
],
regions: [],
helpOptions: [],
showModal: false,
phoneError: '',
......@@ -214,14 +211,11 @@ export default {
this.checkFormValidity();
},
checkFormValidity() {
// Checks if there are any errors present in the form
this.formHasErrors =
this.phoneError !== '' ||
this.streetNrError !== '' ||
this.dobError !== '' ||
this.postalCodeError !== ''
},
// Fetch help options from the backend
async fetchHelpOptions() {
try {
const response = await fetch('http://localhost:8080/api/tags/helpOptions');
......@@ -235,8 +229,19 @@ export default {
console.error('Failed to fetch help options:', error);
}
},
// Submit the application to the backend
// Inset new application and associate selected help options with the application
async fetchRegions() {
try {
const response = await fetch('http://localhost:8080/api/regions/all');
if (response.ok) {
this.regions = await response.json();
} else {
console.error('Error response from server:', response.statusText);
}
} catch (error) {
console.error('Failed to fetch regions:', error);
}
},
async submitApplication() {
try {
const applicationData = {
......@@ -317,11 +322,12 @@ export default {
}
},
closeModal() {
this.showModal = false; // Close the modal when the close button is clicked
this.showModal = false;
}
},
created() {
this.fetchHelpOptions(); // Fetch help options when the component is created
this.fetchHelpOptions();
this.fetchRegions();
}
};
</script>
......
<template>
<div class="settings-page">
<h2>Seaded</h2>
<h3>Siin lehel on võimalus vaadata ja lisada regioone, liigigruppe, alamliike, kasutajate rolle ja funktsioone.</h3>
<div class="settings-container">
<div class="section" v-for="(section, index) in sections" :key="index">
<h3>{{ section.title }}</h3>
<div class="view-container">
<ul>
<li v-for="(item, i) in section.items" :key="i">{{ item }}</li>
</ul>
<span v-for="(item, i) in section.items" :key="i">{{ item }}</span>
</div>
<div class="form-field">
<label for="input-{{ index }}">{{ section.inputLabel }}</label>
......@@ -37,9 +36,7 @@
</select>
</div>
<div class="view-container">
<ul>
<li v-for="(species, i) in speciesItems" :key="i">{{ species }}</li>
</ul>
<span v-for="(species, i) in speciesItems" :key="i">{{ species }}</span>
</div>
<div class="form-field">
<label for="species-input">Uus liik:</label>
......@@ -70,6 +67,8 @@ export default {
inputValue: "",
inputPlaceholder: "Sisesta uus roll",
items: [],
apiEndpoint: '/api/roles/all',
apiAddEndpoint: '/api/roles/add',
},
{
title: "Regioonid",
......@@ -77,6 +76,8 @@ export default {
inputValue: "",
inputPlaceholder: "Sisesta uus regioon",
items: [],
apiEndpoint: '/api/regions/all',
apiAddEndpoint: '/api/regions/add',
},
{
title: "Funtsioonid",
......@@ -84,6 +85,8 @@ export default {
inputValue: "",
inputPlaceholder: "Sisesta uus funktsioon",
items: [],
apiEndpoint: '/api/tags/helpOptions',
apiAddEndpoint: '/api/tags/add',
},
{
title: "Liigigrupid",
......@@ -91,198 +94,126 @@ export default {
inputValue: "",
inputPlaceholder: "Sisesta uus liigigrupp",
items: [],
apiEndpoint: '/api/upperSpecies/all',
apiAddEndpoint: '/api/upperSpecies/add',
}
],
upperSpeciesList: [], // To hold the list of upper species
selectedUpperSpecies: null, // To hold the selected upper species
speciesItems: [], // To hold the species based on the selected upper species
upperSpeciesList: [],
selectedUpperSpecies: null,
speciesItems: [],
newSpeciesInput: '',
alertShown: false
};
},
created() {
this.fetchRegions();
this.fetchTags();
this.fetchRoles();
this.sections.forEach((section, index) => {
this.fetchData(section.apiEndpoint, index);
});
this.fetchUpperSpecies();
},
methods: {
getAuthToken() {
return localStorage.getItem('token'); // Example using local storage
},
async fetchRoles() {
async fetchData(endpoint, sectionIndex) {
const token = this.getAuthToken();
console.log('Token:', token);
const response = await fetch('http://localhost:8080/api/roles/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const roles = await response.json();
this.sections[0].items = roles.map(role => role.role);
} else {
console.error('Failed to fetch roles:', response.statusText);
}
},
async fetchRegions() {
const token = this.getAuthToken();
console.log('Token:', token);
const response = await fetch('http://localhost:8080/api/regions/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
this.sections[1].items = await response.json(); // Assuming regions have a 'name' field
} else {
console.error('Failed to fetch regions:', response.statusText);
}
},
async fetchTags() {
const token = this.getAuthToken();
console.log('Token:', token);
const response = await fetch('http://localhost:8080/api/tags/helpOptions', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
try {
const response = await fetch(`http://localhost:8080${endpoint}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
this.sections[sectionIndex].items = data.map(item => item.name || item.function || item.role || item);
} else if (response.status === 403) {
this.handleUnauthorized();
}
});
if (response.ok) {
const tags = await response.json();
this.sections[2].items = tags.map(tag => tag.function); // Assuming tags have a 'function' field
} else {
console.error('Failed to fetch tags:', response.statusText);
} catch (error) {
console.error(`Failed to fetch data from ${endpoint}:`, error);
}
},
async fetchUpperSpecies() {
const token = this.getAuthToken();
console.log('Token:', token);
const response = await fetch('http://localhost:8080/api/upperSpecies/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const species = await response.json();
this.sections[3].items = species.map(species => species.name); // Assuming species have a 'name' field
this.upperSpeciesList = species.map(species => species.name);
} else {
console.error('Failed to fetch species:', response.statusText);
}
},
async fetchSpecies() {
if (!this.selectedUpperSpecies) return; // Do nothing if no upper species is selected
const token = this.getAuthToken();
console.log('Token:', token);
const response = await fetch(`http://localhost:8080/api/species/all/${this.selectedUpperSpecies}`, { // Use template literal
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const species = await response.json();
this.speciesItems = species.map(species => species.name); // Assuming species have a 'name' field
console.log('Species:', this.speciesItems);
} else {
console.error('Failed to fetch species:', response.statusText);
}
await this.fetchData('/api/upperSpecies/all', 3);
this.upperSpeciesList = this.sections[3].items;
},
async handleAdd(section) {
if (section.inputValue) {
const token = this.getAuthToken();
const body = { name: section.inputValue };
if (section.title === 'Rollid') {
const response = await fetch('http://localhost:8080/api/roles/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body)
});
if (response.ok) {
section.items.push(section.inputValue);
section.inputValue = '';
} else {
console.error('Failed to add role:', response.statusText);
}
} else if (section.title === 'Regioonid') {
const response = await fetch('http://localhost:8080/api/regions/add', {
try {
const response = await fetch(`http://localhost:8080${section.apiAddEndpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body)
body: JSON.stringify({ name: section.inputValue })
});
if (response.ok) {
section.items.push(section.inputValue);
section.inputValue = '';
} else {
console.error('Failed to add region:', response.statusText);
location.reload();
} else if (response.status === 403) {
this.handleUnauthorized();
}
} else if (section.title === 'Funtsioonid') {
const response = await fetch('http://localhost:8080/api/tags/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body)
});
if (response.ok) {
section.items.push(section.inputValue);
section.inputValue = '';
} else {
console.error('Failed to add tag:', response.statusText);
} catch (error) {
console.error(`Failed to add item to ${section.title}:`, error);
}
}
},
handleUnauthorized() {
if (this.alertShown) return;
console.error('Unauthorized: session expired');
alert('Teie sessioon on aegunud, palun logige uuesti sisse!');
this.$store.dispatch('logout');
this.$router.push('/login');
this.alertShown = true;
},
async fetchSpecies() {
if (!this.selectedUpperSpecies) return;
const token = this.getAuthToken();
try {
const response = await fetch(`http://localhost:8080/api/species/all/${this.selectedUpperSpecies}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
} else if (section.title === 'Liigigrupid') {
const response = await fetch('http://localhost:8080/api/upperSpecies/add', {
});
if (response.ok) {
this.speciesItems = (await response.json()).map(species => species.name);
} else if (response.status === 403) {
this.handleUnauthorized();
}
} catch (error) {
console.error('Failed to fetch species:', error);
}
},
async handleAddSpecies() {
if (this.newSpeciesInput && this.selectedUpperSpecies) {
const token = this.getAuthToken();
try {
const response = await fetch('http://localhost:8080/api/species/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body)
body: JSON.stringify({
name: this.newSpeciesInput,
upperSpeciesName: this.selectedUpperSpecies
})
});
if (response.ok) {
section.items.push(section.inputValue);
section.inputValue = '';
} else {
console.error('Failed to add species group:', response.statusText);
this.speciesItems.push(this.newSpeciesInput);
this.newSpeciesInput = '';
} else if (response.status === 403) {
this.handleUnauthorized();
}
}
}
},
async handleAddSpecies() {
if (this.newSpeciesInput && this.selectedUpperSpecies) {
const token = this.getAuthToken();
const response = await fetch('http://localhost:8080/api/species/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
name: this.newSpeciesInput,
upperSpeciesName: this.selectedUpperSpecies
})
});
if (response.ok) {
this.speciesItems.push(this.newSpeciesInput);
this.newSpeciesInput = '';
} else {
console.error('Failed to add species:', response.statusText);
} catch (error) {
console.error('Failed to add species:', error);
}
}
}
......@@ -325,7 +256,8 @@ h3 {
justify-content: flex-start; /* Align items to the start */
}
.form-field {
.form-field,
.button-container {
margin-bottom: 5px; /* Reduced bottom margin */
display: flex;
flex-direction: column; /* Stack label and input vertically */
......@@ -346,9 +278,6 @@ label {
font-size: 0.9em; /* Slightly smaller font size */
}
.button-container {
text-align: center; /* Center the button */
}
button {
background-color: #87D26E;
......@@ -366,5 +295,18 @@ button:hover {
.view-container {
margin-top: 5px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.view-container span {
margin: 5px;
background-color: #F1F4F1;
border-radius: 5px;
display: inline-flex;
padding: 10px 20px;
font-size: 1.1em;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.15);
}
</style>
......@@ -145,125 +145,85 @@ export default {
};
},
async created() {
await this.fetchRegions();
await this.fetchUsers();
await this.fetchTags();
await this.fetchSpecies();
await Promise.all([
this.fetchRegions(),
this.fetchUsers(),
this.fetchTags(),
this.fetchSpecies(),
]);
},
watch: {
selectedRegions: 'filterUsers',
searchTerm: 'filterUsers',
selectedTags: 'filterUsers',
selectedSpecies: 'filterUsers',
alertShown: false,
},
methods: {
getAuthToken() {
return localStorage.getItem('token'); // Get the token from local storage
},
async fetchRegions() {
handleUnauthorized() {
if (this.alertShown) return;
alert('Teie sessioon on aegunud, palun logige uuesti sisse!');
this.$store.dispatch('logout');
this.$router.push('/login');
this.alertShown = true;
},
async fetchData(endpoint) {
const token = this.getAuthToken();
console.log('Token:', token);
const response = await fetch('http://localhost:8080/api/regions/all', {
method: 'GET', // Ensure you're using the correct HTTP method
const response = await fetch(`http://localhost:8080/api/${endpoint}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // Include the JWT token in the Authorization header
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
this.regions = await response.json();
} else {
console.error('Failed to fetch regions:', response.statusText);
if (!response.ok) {
if (response.status === 403) {
this.handleUnauthorized();
} else {
console.error(`Failed to fetch data`);
}
return null;
}
return response.json();
},
async fetchRegions() {
this.regions = await this.fetchData('regions/all');
},
async fetchTags() {
const token = this.getAuthToken();
console.log('Token:', token);
const response = await fetch('http://localhost:8080/api/tags/helpOptions', {
method: 'GET', // Ensure you're using the correct HTTP method
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // Include the JWT token in the Authorization header
}
});
if (response.ok) {
const tags = await response.json();
this.tags = tags.map(tag => tag.function);
} else {
console.error('Failed to fetch tags:', response.statusText);
}
const tags = await this.fetchData('tags/helpOptions');
this.tags = tags ? tags.map(tag => tag.function) : [];
},
async fetchSpecies() {
const token = this.getAuthToken();
console.log('Token:', token);
const response = await fetch('http://localhost:8080/api/upperSpecies/all', {
method: 'GET', // Ensure you're using the correct HTTP method
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // Include the JWT token in the Authorization header
}
});
if (response.ok) {
const species = await response.json();
this.species = species.map(species => species.name);
} else {
console.error('Failed to fetch species:', response.statusText);
}
const species = await this.fetchData('upperSpecies/all');
this.species = species ? species.map(species => species.name) : [];
},
async fetchUsers() {
const token = this.getAuthToken(); // Use the method to get the token
const response = await fetch('http://localhost:8080/api/users/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // Include the token in the headers
}
});
const users = await this.fetchData('users/all');
if (!users) return;
if (!response.ok) {
const errorMessage = await response.text();
console.error('Failed to fetch users:', response.statusText, errorMessage);
return;
}
const users = await response.json();
const usersWithDetails = await Promise.all(users.map(async (user) => {
const [rolesResponse, tagsResponse, regionsResponse, speciesResponse] = await Promise.all([
fetch(`http://localhost:8080/api/users/${user.id}/roles`, {
headers: { 'Authorization': `Bearer ${token}` }
}),
fetch(`http://localhost:8080/api/users/${user.id}/tags`, {
headers: { 'Authorization': `Bearer ${token}` }
}),
fetch(`http://localhost:8080/api/users/${user.id}/regions`, {
headers: { 'Authorization': `Bearer ${token}` }
}),
fetch(`http://localhost:8080/api/users/${user.id}/species`, {
headers: { 'Authorization': `Bearer ${token}` }
}),
]);
const roles = await rolesResponse.json();
const tags = await tagsResponse.json();
const regions = await regionsResponse.json();
const species = await speciesResponse.json();
this.users = await Promise.all(users.map(async (user) => {
const roles = await this.fetchData(`users/${user.id}/roles`);
const tags = await this.fetchData(`users/${user.id}/tags`);
const regions = await this.fetchData(`users/${user.id}/regions`);
const species = await this.fetchData(`users/${user.id}/species`);
return {
...user,
roles,
tags,
regions,
species
roles: roles || [],
tags: tags || [],
regions: regions || [],
species: species || []
};
}));
this.users = usersWithDetails;
this.filteredUsers = usersWithDetails;
this.filteredUsers = this.users;
},
filterUsers() {
this.filteredUsers = this.users.filter(user => {
// Region filtering: If no region is selected, regionMatch is true
const regionMatch = this.selectedRegions.length === 0 || user.regions.some(region =>
this.selectedRegions.some(selectedRegion => selectedRegion === region)
);
......@@ -274,13 +234,9 @@ export default {
const speciesMatch = this.selectedSpecies.length === 0 || user.species.some(species =>
this.selectedSpecies.some(selectedSpecies => selectedSpecies === species.name)
);
// Name search filtering: If no name is entered, nameMatch is true
const nameMatch = this.searchTerm.trim() === '' ||
user.firstName.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
user.lastName.toLowerCase().includes(this.searchTerm.toLowerCase());
// Either regionMatch or nameMatch (or both) must be true for the user to appear
return regionMatch && nameMatch && tagMatch && speciesMatch;
});
},
......
......@@ -59,12 +59,14 @@ public class SecurityConfig {
.ignoringRequestMatchers("/api/**")) // CSRF ignored for API endpoints
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/login").permitAll() // Allow unauthenticated access to /api/login
.requestMatchers("/api/applications/addApplication").permitAll()
.requestMatchers("/api/applicationToTags/addApplicationToTag").permitAll()
.requestMatchers("/api/regions/all").permitAll()
.requestMatchers("/api/tags/helpOptions").permitAll()
.anyRequest().authenticated()) // Protect other requests
.addFilter(customAuthenticationFilter)
.addFilterBefore(new JwtAuthorizationFilter(jwtSecret, userDetailsService), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
......@@ -9,7 +9,7 @@ public class ApplicationDTO {
private String email;
private Date dob;
private String street;
private int streetNr;
private String streetNr;
private String city;
private String region;
private String postalCode;
......@@ -66,11 +66,11 @@ public class ApplicationDTO {
this.street = street;
}
public int getStreetNr() {
public String getStreetNr() {
return streetNr;
}
public void setStreetNr(int streetNr) {
public void setStreetNr(String streetNr) {
this.streetNr = streetNr;
}
......
......@@ -22,7 +22,7 @@ public class Application {
private String email;
private Date birthDate;
private String streetName;
private int streetNr;
private String streetNr;
private String city;
private String county;
private String postalCode;
......@@ -34,7 +34,7 @@ public class Application {
public Application() {
}
public Application(UUID id, Boolean isAccepted, String firstName, String lastName, String phoneNr, String email, Date birthDate, String streetName, int streetNr, String city, String county, String postalCode, String question1, String question2, String password) {
public Application(UUID id, Boolean isAccepted, String firstName, String lastName, String phoneNr, String email, Date birthDate, String streetName, String streetNr, String city, String county, String postalCode, String question1, String question2, String password) {
this.id = id;
this.isAccepted = isAccepted;
this.firstName = firstName;
......@@ -120,11 +120,11 @@ public class Application {
this.streetName = streetName;
}
public int getStreetNr() {
public String getStreetNr() {
return streetNr;
}
public void setStreetNr(int streetNr) {
public void setStreetNr(String streetNr) {
this.streetNr = streetNr;
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment