diff --git a/src/App.vue b/src/App.vue
index 9d87945a..1bffc112 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -3,5 +3,13 @@
diff --git a/src/boot/api.ts b/src/boot/api.ts
index 381407d3..48af9638 100644
--- a/src/boot/api.ts
+++ b/src/boot/api.ts
@@ -8,12 +8,18 @@ import {
ObservationsApi,
TaxaApi,
BoundariesApi,
+ AuthApi,
+ UsersApi,
} from 'mosquito-alert';
import { BASE_PATH } from 'mosquito-alert/base';
+import { attachAuthInterceptor } from 'mosquito-alert/interceptors';
+import { useAuthStore } from 'src/stores/authStore';
const apiConfig = new Configuration({
...(import.meta.env.VITE_API_BASE_URL ? { basePath: import.meta.env.VITE_API_BASE_URL } : {}),
+ // accessToken: localStorage.getItem('access_token') ?? undefined, // () => localStorage.getItem('access_token') || undefined
});
+
const apiUrl = apiConfig.basePath || BASE_PATH;
const axiosInstance = axios.create({});
@@ -23,6 +29,8 @@ const bitesApi = new BitesApi(apiConfig, undefined, axiosInstance);
const breedingSitesApi = new BreedingSitesApi(apiConfig, undefined, axiosInstance);
const observationsApi = new ObservationsApi(apiConfig, undefined, axiosInstance);
const boundariesApi = new BoundariesApi(apiConfig, undefined, axiosInstance);
+const authApi = new AuthApi(apiConfig, undefined, axiosInstance);
+const userApi = new UsersApi(apiConfig, undefined, axiosInstance);
export default boot(({ app }) => {
const i18n = app.config.globalProperties.$i18n;
@@ -37,6 +45,17 @@ export default boot(({ app }) => {
return config;
});
+ attachAuthInterceptor(axiosInstance, {
+ configuration: apiConfig,
+ refreshToken: () => localStorage.getItem('refresh_token') || '',
+ updateAccessToken: (newAccessToken) => {
+ localStorage.setItem('access_token', newAccessToken);
+ // Also update the store to maintain reactivity
+ const authStore = useAuthStore();
+ authStore.accessToken = newAccessToken;
+ },
+ });
+
// Make available in Options API components as this.$api...
app.config.globalProperties.$api = {
taxaApi,
@@ -44,7 +63,18 @@ export default boot(({ app }) => {
breedingSitesApi,
observationsApi,
boundariesApi,
+ authApi,
+ userApi,
};
});
-export { apiUrl, taxaApi, bitesApi, breedingSitesApi, observationsApi, boundariesApi };
+export {
+ apiUrl,
+ taxaApi,
+ bitesApi,
+ breedingSitesApi,
+ observationsApi,
+ boundariesApi,
+ authApi,
+ userApi,
+};
diff --git a/src/components/layout/MainLeftDrawer.vue b/src/components/layout/MainLeftDrawer.vue
index 367fc661..daa975c8 100644
--- a/src/components/layout/MainLeftDrawer.vue
+++ b/src/components/layout/MainLeftDrawer.vue
@@ -31,6 +31,8 @@
+
+
@@ -47,12 +49,13 @@
import { ref } from 'vue'
import { useQuasar } from 'quasar'
-import { LanguageSwitcher, ShareButton } from './MainLeftDrawer'
+import { LanguageSwitcher, ShareButton, AuthButton } from './MainLeftDrawer'
export default {
components: {
LanguageSwitcher,
- ShareButton
+ ShareButton,
+ AuthButton
},
setup() {
diff --git a/src/components/layout/MainLeftDrawer/AuthButton.vue b/src/components/layout/MainLeftDrawer/AuthButton.vue
new file mode 100644
index 00000000..c84d640a
--- /dev/null
+++ b/src/components/layout/MainLeftDrawer/AuthButton.vue
@@ -0,0 +1,79 @@
+
+
+
+
+ {{ getInitials(userStore.user?.full_name || '') }}
+
+
+
+
+
+
+ {{ $t('logout') }}
+
+
+
+
+
+
+
+
+
+ {{ $t('login') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/layout/MainLeftDrawer/index.ts b/src/components/layout/MainLeftDrawer/index.ts
index b64aacec..b93efca4 100644
--- a/src/components/layout/MainLeftDrawer/index.ts
+++ b/src/components/layout/MainLeftDrawer/index.ts
@@ -1,2 +1,3 @@
+export { default as AuthButton } from './AuthButton.vue';
export { default as LanguageSwitcher } from './LanguageSwitcher.vue';
export { default as ShareButton } from './ShareButton.vue';
diff --git a/src/components/reports/BaseReportDetailDrawer.vue b/src/components/reports/BaseReportDetailDrawer.vue
index aaaca0bf..9153ca68 100644
--- a/src/components/reports/BaseReportDetailDrawer.vue
+++ b/src/components/reports/BaseReportDetailDrawer.vue
@@ -69,6 +69,15 @@
+
+
+
+
+
+
+ Public
+
+
diff --git a/src/components/reports/BaseReportVectorLayer.vue b/src/components/reports/BaseReportVectorLayer.vue
index b1fa0038..cf2be3b2 100644
--- a/src/components/reports/BaseReportVectorLayer.vue
+++ b/src/components/reports/BaseReportVectorLayer.vue
@@ -21,7 +21,9 @@ import type { ReportType } from 'src/types/reportType';
import { getHistogramDateKey } from 'src/components/reports/analytics/utils';
import type VectorSource from 'ol/source/Vector';
import type { BiteGeoModel, BreedingSiteGeoModel, ObservationGeoModel } from 'mosquito-alert';
+import { useAuthStore } from 'src/stores/authStore';
+const authStore = useAuthStore();
const layerRef = ref<{ webglVectorLayer: Layer }>();
const sourceRef = ref<{ source: VectorSource }>();
@@ -77,6 +79,15 @@ watch(() => props.visible, (newValue) => {
layerRef.value.webglVectorLayer.setVisible(newValue === true)
})
+// Refresh the layer when authentication state changes (e.g., after login)
+// This ensures authenticated users see observations they have access to
+watch(() => authStore.isAuthenticated, (newValue, oldValue) => {
+ // Refresh when authentication state changes (login or logout)
+ if (newValue !== oldValue) {
+ refresh();
+ }
+})
+
const style = {
"circle-fill-color": ['get', 'color'],
'circle-radius': [
diff --git a/src/i18n/ca/index.ts b/src/i18n/ca/index.ts
index c2bb3d48..cbf5acb9 100644
--- a/src/i18n/ca/index.ts
+++ b/src/i18n/ca/index.ts
@@ -2,6 +2,14 @@ import { BreedingSiteSiteType, BiteEventEnvironment, BiteEventMoment } from 'mos
import { ReportType } from 'src/types/reportType';
export default {
+ login: 'Iniciar sessió',
+ login_failed: 'Error en iniciar sessió',
+ logout: 'Tancar sessió',
+ username: "Nom d'usuari",
+ username_required: "Nom d'usuari requerit",
+ password_required: 'Contrasenya requerida',
+ password: 'Contrasenya',
+ cancel: 'Cancel·lar',
footer_collaborators_note: 'Aquest mapa interactiu ha estat finançat per',
url_copied: 'URL copiat al porta-retalls',
observations: 'Observacions',
diff --git a/src/i18n/en-US/index.ts b/src/i18n/en-US/index.ts
index 335a1d7a..fe7107bb 100644
--- a/src/i18n/en-US/index.ts
+++ b/src/i18n/en-US/index.ts
@@ -2,6 +2,14 @@ import { BreedingSiteSiteType, BiteEventEnvironment, BiteEventMoment } from 'mos
import { ReportType } from 'src/types/reportType';
export default {
+ login: 'Login',
+ login_failed: 'Login failed',
+ logout: 'Logout',
+ username: 'Username',
+ username_required: 'Username required',
+ password_required: 'Password required',
+ password: 'Password',
+ cancel: 'Cancel',
footer_collaborators_note: 'This interactive map has been funded by',
url_copied: 'URL copied to clipboard',
observations: 'Citizen observations',
diff --git a/src/i18n/es/index.ts b/src/i18n/es/index.ts
index 5b925076..afde6b6a 100644
--- a/src/i18n/es/index.ts
+++ b/src/i18n/es/index.ts
@@ -2,6 +2,14 @@ import { BreedingSiteSiteType, BiteEventEnvironment, BiteEventMoment } from 'mos
import { ReportType } from 'src/types/reportType';
export default {
+ login: 'Iniciar sesión',
+ login_failed: 'Error al iniciar sesión',
+ logout: 'Cerrar sesión',
+ username: 'Nombre de usuario',
+ username_required: 'Nombre de usuario requerido',
+ password_required: 'Contraseña requerida',
+ password: 'Contraseña',
+ cancel: 'Cancelar',
footer_collaborators_note: 'Este mapa interactivo ha sido financiado por',
url_copied: 'URL copiada al portapapeles',
observations: 'Observaciones',
diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts
new file mode 100644
index 00000000..281f68c9
--- /dev/null
+++ b/src/stores/authStore.ts
@@ -0,0 +1,73 @@
+import { defineStore } from 'pinia';
+
+import { authApi } from 'src/boot/api';
+import type { AuthApiObtainTokenRequest } from 'mosquito-alert';
+
+import { useUserStore } from './userStore';
+
+export const useAuthStore = defineStore('auth', {
+ state: () => ({
+ accessToken: null as string | null,
+ refreshToken: null as string | null,
+ }),
+ actions: {
+ async login(username: string, password: string) {
+ try {
+ const response = await authApi.obtainToken({
+ appUserTokenObtainPairRequest: {
+ username: username,
+ password: password,
+ },
+ } as AuthApiObtainTokenRequest);
+
+ // Update both state and localStorage
+ this.accessToken = response.data.access;
+ this.refreshToken = response.data.refresh;
+ localStorage.setItem('access_token', response.data.access);
+ localStorage.setItem('refresh_token', response.data.refresh);
+
+ // Load user info after login
+ const userStore = useUserStore();
+ await userStore.fetchCurrentUser();
+ } catch (error) {
+ console.error('Login failed:', error);
+ // Handle login error (e.g., show a notification)
+ throw error;
+ }
+ },
+ logout() {
+ // Clear both state and localStorage
+ this.accessToken = null;
+ this.refreshToken = null;
+ localStorage.removeItem('access_token');
+ localStorage.removeItem('refresh_token');
+
+ const userStore = useUserStore();
+ userStore.clearUser();
+ },
+ async initialize() {
+ // Check if tokens exist in localStorage on app start
+ const accessToken = localStorage.getItem('access_token');
+ const refreshToken = localStorage.getItem('refresh_token');
+
+ if (accessToken && refreshToken) {
+ // Restore tokens to state
+ this.accessToken = accessToken;
+ this.refreshToken = refreshToken;
+
+ // Fetch user info
+ const userStore = useUserStore();
+ try {
+ await userStore.fetchCurrentUser();
+ } catch (error) {
+ // If fetching user fails, tokens might be invalid - clear them
+ console.error('Failed to fetch user on initialization:', error);
+ this.logout();
+ }
+ }
+ },
+ },
+ getters: {
+ isAuthenticated: (state) => !!state.accessToken,
+ },
+});
diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts
new file mode 100644
index 00000000..f085479c
--- /dev/null
+++ b/src/stores/userStore.ts
@@ -0,0 +1,26 @@
+import { defineStore } from 'pinia';
+import type { RawAxiosRequestConfig } from 'axios';
+
+import { userApi } from 'src/boot/api';
+import type { User } from 'mosquito-alert';
+
+export const useUserStore = defineStore('user', {
+ state: () => ({
+ user: null as User | null,
+ }),
+ actions: {
+ async fetchCurrentUser(options?: RawAxiosRequestConfig) {
+ try {
+ const response = await userApi.retrieveMine(options);
+ this.user = response.data;
+ } catch (error) {
+ console.error('Failed to fetch user:', error);
+ this.clearUser();
+ throw error;
+ }
+ },
+ clearUser() {
+ this.user = null;
+ },
+ },
+});
diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts
new file mode 100644
index 00000000..1f20a2fe
--- /dev/null
+++ b/src/utils/Utils.ts
@@ -0,0 +1,8 @@
+export function getInitials(fullName: string): string {
+ return fullName
+ .split(' ')
+ .filter((word) => word.length > 0)
+ .map((word) => word[0]?.toUpperCase())
+ .slice(0, 2)
+ .join('');
+}