110 lines
3.0 KiB
TypeScript
110 lines
3.0 KiB
TypeScript
|
|
import { create } from 'zustand';
|
||
|
|
import { config } from '../config';
|
||
|
|
|
||
|
|
interface AuthState {
|
||
|
|
accessToken: string | null;
|
||
|
|
refreshToken: string | null;
|
||
|
|
username: string | null;
|
||
|
|
isAuthenticated: boolean;
|
||
|
|
error: string | null;
|
||
|
|
loading: boolean;
|
||
|
|
login: (username: string, password: string) => Promise<void>;
|
||
|
|
refresh: () => Promise<boolean>;
|
||
|
|
logout: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadTokens() {
|
||
|
|
return {
|
||
|
|
accessToken: localStorage.getItem('cameleer-access-token'),
|
||
|
|
refreshToken: localStorage.getItem('cameleer-refresh-token'),
|
||
|
|
username: localStorage.getItem('cameleer-username'),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function persistTokens(access: string, refresh: string, username: string) {
|
||
|
|
localStorage.setItem('cameleer-access-token', access);
|
||
|
|
localStorage.setItem('cameleer-refresh-token', refresh);
|
||
|
|
localStorage.setItem('cameleer-username', username);
|
||
|
|
}
|
||
|
|
|
||
|
|
function clearTokens() {
|
||
|
|
localStorage.removeItem('cameleer-access-token');
|
||
|
|
localStorage.removeItem('cameleer-refresh-token');
|
||
|
|
localStorage.removeItem('cameleer-username');
|
||
|
|
}
|
||
|
|
|
||
|
|
const initial = loadTokens();
|
||
|
|
|
||
|
|
export const useAuthStore = create<AuthState>((set, get) => ({
|
||
|
|
accessToken: initial.accessToken,
|
||
|
|
refreshToken: initial.refreshToken,
|
||
|
|
username: initial.username,
|
||
|
|
isAuthenticated: !!initial.accessToken,
|
||
|
|
error: null,
|
||
|
|
loading: false,
|
||
|
|
|
||
|
|
login: async (username, password) => {
|
||
|
|
set({ loading: true, error: null });
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${config.apiBaseUrl}/auth/login`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ username, password }),
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const body = await res.json().catch(() => ({}));
|
||
|
|
throw new Error(body.message || 'Invalid credentials');
|
||
|
|
}
|
||
|
|
const { accessToken, refreshToken } = await res.json();
|
||
|
|
persistTokens(accessToken, refreshToken, username);
|
||
|
|
set({
|
||
|
|
accessToken,
|
||
|
|
refreshToken,
|
||
|
|
username,
|
||
|
|
isAuthenticated: true,
|
||
|
|
loading: false,
|
||
|
|
});
|
||
|
|
} catch (e: unknown) {
|
||
|
|
set({
|
||
|
|
error: e instanceof Error ? e.message : 'Login failed',
|
||
|
|
loading: false,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
refresh: async () => {
|
||
|
|
const { refreshToken } = get();
|
||
|
|
if (!refreshToken) return false;
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${config.apiBaseUrl}/auth/refresh`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ refreshToken }),
|
||
|
|
});
|
||
|
|
if (!res.ok) return false;
|
||
|
|
const data = await res.json();
|
||
|
|
const username = get().username ?? '';
|
||
|
|
persistTokens(data.accessToken, data.refreshToken, username);
|
||
|
|
set({
|
||
|
|
accessToken: data.accessToken,
|
||
|
|
refreshToken: data.refreshToken,
|
||
|
|
isAuthenticated: true,
|
||
|
|
});
|
||
|
|
return true;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
logout: () => {
|
||
|
|
clearTokens();
|
||
|
|
set({
|
||
|
|
accessToken: null,
|
||
|
|
refreshToken: null,
|
||
|
|
username: null,
|
||
|
|
isAuthenticated: false,
|
||
|
|
error: null,
|
||
|
|
});
|
||
|
|
},
|
||
|
|
}));
|