Initial commit
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type Role = 'USER' | 'ADMIN';
|
||||
export type AuthProviderName = 'LOCAL' | 'GOOGLE' | 'FACEBOOK';
|
||||
export type AccountType = 'PERSONAL' | 'COMPANY';
|
||||
export type ContactPreference = 'EMAIL' | 'PHONE' | 'EMAIL_AND_PHONE';
|
||||
export type PreferredLanguage = 'PL' | 'EN' | 'UK' | 'DE';
|
||||
|
||||
export type AuthUser = {
|
||||
id: number;
|
||||
email: string;
|
||||
fullName: string;
|
||||
role: Role;
|
||||
provider: AuthProviderName;
|
||||
accountType: AccountType;
|
||||
phone: string | null;
|
||||
address: string | null;
|
||||
contactPreference: ContactPreference;
|
||||
preferredLanguage: PreferredLanguage;
|
||||
nip: string | null;
|
||||
birthDate: string | null;
|
||||
verified: boolean;
|
||||
blocked: boolean;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type AuthResponse = { token: string; user: AuthUser };
|
||||
|
||||
export type RegisterDetails = {
|
||||
accountType: AccountType;
|
||||
phone?: string;
|
||||
nip?: string;
|
||||
};
|
||||
|
||||
type AuthContextValue = {
|
||||
user: AuthUser | null;
|
||||
token: string | null;
|
||||
loading: boolean;
|
||||
login: (email: string, password: string) => Promise<AuthUser>;
|
||||
register: (email: string, password: string, fullName: string, details?: RegisterDetails) => Promise<AuthUser>;
|
||||
socialLogin: (provider: Exclude<AuthProviderName, 'LOCAL'>) => Promise<AuthUser>;
|
||||
updateProfile: (fullName: string, phone?: string, birthDate?: string, address?: string, contactPreference?: ContactPreference, preferredLanguage?: PreferredLanguage) => Promise<AuthUser>;
|
||||
logout: () => void;
|
||||
};
|
||||
|
||||
const TOKEN_KEY = 'polskalokalnie-auth-token';
|
||||
const API_BASE = '/api';
|
||||
|
||||
export function getToken(): string | null {
|
||||
return window.localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cienki wrapper na fetch: dokleja token Bearer i zamienia bledy backendu (pole "message")
|
||||
* na wyjatek z czytelnym komunikatem po polsku.
|
||||
*/
|
||||
export async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const token = getToken();
|
||||
const headers = new Headers(options.headers);
|
||||
if (options.body && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Wystąpił błąd. Spróbuj ponownie.';
|
||||
try {
|
||||
const data = await response.json();
|
||||
if (data && typeof data.message === 'string' && data.message.trim()) {
|
||||
message = data.message;
|
||||
}
|
||||
} catch {
|
||||
// brak/niepoprawne cialo JSON - zostaje komunikat domyslny
|
||||
}
|
||||
const error = new Error(message) as Error & { status?: number };
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [token, setToken] = useState<string | null>(() => getToken());
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(() => Boolean(getToken()));
|
||||
|
||||
// Po odswiezeniu strony: majac token w localStorage, pobierz aktualny profil.
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setUser(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
apiFetch<AuthUser>('/auth/me')
|
||||
.then((me) => {
|
||||
if (!cancelled) {
|
||||
setUser(me);
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
window.localStorage.removeItem(TOKEN_KEY);
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
const applyAuth = useCallback((auth: AuthResponse) => {
|
||||
window.localStorage.setItem(TOKEN_KEY, auth.token);
|
||||
setToken(auth.token);
|
||||
setUser(auth.user);
|
||||
return auth.user;
|
||||
}, []);
|
||||
|
||||
const login = useCallback(
|
||||
async (email: string, password: string) =>
|
||||
applyAuth(await apiFetch<AuthResponse>('/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password }),
|
||||
})),
|
||||
[applyAuth],
|
||||
);
|
||||
|
||||
const register = useCallback(
|
||||
async (email: string, password: string, fullName: string, details?: RegisterDetails) =>
|
||||
applyAuth(await apiFetch<AuthResponse>('/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password, fullName, ...details }),
|
||||
})),
|
||||
[applyAuth],
|
||||
);
|
||||
|
||||
const socialLogin = useCallback(
|
||||
async (provider: Exclude<AuthProviderName, 'LOCAL'>) =>
|
||||
applyAuth(await apiFetch<AuthResponse>('/auth/social', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ provider }),
|
||||
})),
|
||||
[applyAuth],
|
||||
);
|
||||
|
||||
const updateProfile = useCallback(
|
||||
async (fullName: string, phone?: string, birthDate?: string, address?: string, contactPreference?: ContactPreference, preferredLanguage?: PreferredLanguage) => {
|
||||
const updated = await apiFetch<AuthUser>('/auth/me', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ fullName, phone, birthDate, address, contactPreference, preferredLanguage }),
|
||||
});
|
||||
setUser(updated);
|
||||
return updated;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
window.localStorage.removeItem(TOKEN_KEY);
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AuthContextValue>(
|
||||
() => ({ user, token, loading, login, register, socialLogin, updateProfile, logout }),
|
||||
[user, token, loading, login, register, socialLogin, updateProfile, logout],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth(): AuthContextValue {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user