diff --git a/.gitignore b/.gitignore index 5ef6a52..b26dbc5 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,9 @@ yarn-error.log* # vercel .vercel +# IDE +.idea/ + # typescript *.tsbuildinfo next-env.d.ts diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 0000000..aa4de66 --- /dev/null +++ b/lib/api.ts @@ -0,0 +1,175 @@ +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8080/api'; +import { LoginResponse, User, Association } from "./models"; + +// Store token in localStorage (we'll improve this later) +export function setAuthToken(token: string) { + localStorage.setItem('auth_token', token); +} + +export function getAuthToken(): string | null { + return localStorage.getItem('auth_token'); +} + +export function removeAuthToken() { + localStorage.removeItem('auth_token'); +} + +// Helper to add Authorization header +function getHeaders(): HeadersInit { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + const token = getAuthToken(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + return headers; +} + +export async function login(email: string, password: string): Promise { + const response = await fetch(`${API_BASE_URL}/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + throw new Error('Invalid credentials'); + } + + const data: LoginResponse = await response.json(); + setAuthToken(data.token); // Store token + return data; +} + +export async function getAssociation(id: string): Promise { + const response = await fetch(`${API_BASE_URL}/associations/${id}`, { + headers: getHeaders(), + }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + removeAuthToken(); + throw new Error('Unauthorized'); + } + throw new Error('Failed to fetch association'); + } + + return response.json(); +} + +export async function getAssociations(): Promise { + const response = await fetch(`${API_BASE_URL}/associations`, { + headers: getHeaders(), + }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + removeAuthToken(); + throw new Error('Unauthorized'); + } + throw new Error('Failed to fetch associations'); + } + + return response.json(); +} + +export async function createAssociation(association: Omit): Promise { + const response = await fetch(`${API_BASE_URL}/associations`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(association), + }); + + if (!response.ok) { + throw new Error('Failed to create association'); + } + + return response.json(); +} + +export async function updateAssociation(id: string, association: Association): Promise { + const response = await fetch(`${API_BASE_URL}/associations/${id}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(association), + }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + removeAuthToken(); + throw new Error('Unauthorized'); + } + throw new Error('Failed to update association'); + } + + return response.json(); +} + +export async function downloadAttestato(id: string, associationName: string): Promise { + const response = await fetch(`${API_BASE_URL}/associations/${id}/attestato`, { + headers: getHeaders(), + }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + removeAuthToken(); + throw new Error('Unauthorized'); + } + throw new Error('Failed to generate attestato'); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `attestato-${associationName}.pdf`; + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); +} + +export async function deleteAssociation(id: string): Promise { + const response = await fetch(`${API_BASE_URL}/associations/${id}`, { + method: 'DELETE', + headers: getHeaders(), + }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + removeAuthToken(); + throw new Error('Unauthorized'); + } + throw new Error('Failed to delete association'); + } +} + +export function logout() { + removeAuthToken(); +} + +export async function verifyToken(): Promise { + try { + const token = getAuthToken(); + + if (!token) return false; + + const response = await fetch(`${API_BASE_URL}/associations`, { + headers: getHeaders(), + }); + + if (response.status === 401 || response.status === 403) { + removeAuthToken(); + return false; + } + + return response.ok; + + } catch (error) { + console.error("verifyToken error:", error); + return false; + } +} \ No newline at end of file diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..f37cf00 --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,30 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { logout as clearAuthToken } from "./api"; +import type { User } from "./models"; + +export type StoredUser = Pick; + +export function useStoredUser() { + const [user, setUser] = useState(null); + const [ready, setReady] = useState(false); + + useEffect(() => { + const email = localStorage.getItem("user_email"); + const role = localStorage.getItem("user_role") as StoredUser["role"] | null; + if (email && role) { + setUser({ email, role }); + } + setReady(true); + }, []); + + return { user, ready }; +} + +export function clearStoredUser() { + clearAuthToken(); + localStorage.removeItem("user_role"); + localStorage.removeItem("user_email"); + localStorage.removeItem("association_id"); +} diff --git a/lib/models.ts b/lib/models.ts new file mode 100644 index 0000000..7db35b1 --- /dev/null +++ b/lib/models.ts @@ -0,0 +1,37 @@ +export interface Associate { + name: string; + surname: string; + birthDate: string; + address: string; + fiscalCode: string; + italianCitizen: boolean; + role: 'PRESIDENT' | 'VICE_PRESIDENT' | 'SECRETARY' | 'ASSOCIATE'; + residencePermitNumber?: string; +} + +export interface User { + id?: string; + email: string; + password?: string; + role: 'CLIENT' | 'MANAGER' | 'ADMIN'; +} + +export interface Association { + id?: string; + runtsId?: string; + name: string; + address: string; + created?: string; + isShown?: boolean; + vatNumber?: string; + translit?: string; + associates?: Associate[]; + users?: User[]; + ratificationCode?: string; +} + +export interface LoginResponse { + token: string; + user: User; + association: Association; +} \ No newline at end of file diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/public/logo-encap.jpg b/public/logo-encap.jpg new file mode 100644 index 0000000..102753f Binary files /dev/null and b/public/logo-encap.jpg differ