habits.andr33v.ru/app/composables/useAuth.ts

152 lines
4.5 KiB
TypeScript

// /composables/useAuth.ts
import { computed, watch } from 'vue';
import { useVisitTracker } from './useVisitTracker';
interface User {
id: number;
email: string | null; // Can be null for anonymous users
nickname: string | null;
avatar: string | null;
coins: number;
exp: number;
dailyStreak: number;
soundOn: boolean;
confettiOn: boolean;
createdAt: string;
updatedAt: string;
isAnonymous?: boolean; // Flag to distinguish anonymous users
anonymousSessionId?: string;
}
export function useAuth() {
const user = useState<User | null>('user', () => null);
const initialized = useState('auth_initialized', () => false);
const api = useApi();
const { visitCalled } = useVisitTracker();
// A user is fully authenticated only if they exist and are NOT anonymous.
const isAuthenticated = computed(() => !!user.value && !user.value.isAnonymous);
// A user is anonymous if they exist and have the isAnonymous flag.
const isAnonymous = computed(() => !!user.value && !!user.value.isAnonymous);
// --- This watcher is the new core logic for post-authentication tasks ---
watch(isAuthenticated, (newIsAuthenticated, oldIsAuthenticated) => {
// We only care about the transition from logged-out to logged-in
if (newIsAuthenticated && !oldIsAuthenticated) {
if (!visitCalled.value) {
visitCalled.value = true;
const gameDay = new Date().toISOString().slice(0, 10);
// --- 1. Trigger Daily Visit & Streak Calculation ---
api('/api/user/visit', {
method: 'POST',
body: { gameDay }
}).then(updatedUser => {
if (updatedUser) {
updateUser(updatedUser);
}
}).catch(e => {
console.error('Failed to register daily visit:', e);
visitCalled.value = false; // Allow retrying on next navigation if it failed
});
// --- 2. Trigger Village Tick ---
api('/api/village/tick', { method: 'POST' })
.catch(e => {
console.error('Failed to trigger village tick:', e);
});
}
}
});
/**
* Initializes the authentication state for EXISTING users.
* It should be called once in app.vue.
* It will only try to fetch a logged-in user via /api/auth/me.
* If it fails, the user state remains null.
*/
const initAuth = async () => {
if (initialized.value) return;
try {
const response = await api<{ user: User }>('/auth/me');
if (response.user) {
user.value = { ...response.user, isAnonymous: false };
} else {
user.value = null;
}
} catch (error) {
// It's expected this will fail for non-logged-in users.
user.value = null;
} finally {
initialized.value = true;
}
};
/**
* Starts the onboarding process by creating a new anonymous user.
*/
const startOnboarding = async () => {
try {
const anonymousUserData = await api<User>('/onboarding/initiate', { method: 'POST' });
// Explicitly set isAnonymous to true for robustness
user.value = { ...anonymousUserData, isAnonymous: true };
} catch (anonError) {
console.error('Could not initiate anonymous session:', anonError);
// Optionally, show an error message to the user
user.value = null;
}
};
const register = async (email, password, nickname) => {
await api('/auth/register', {
method: 'POST',
body: { email, password, nickname },
});
// After a successful registration, force a re-fetch of the new user state.
initialized.value = false;
await initAuth();
};
const login = async (email, password) => {
await api('/auth/login', {
method: 'POST',
body: { email, password },
});
// After a successful login, force a re-fetch of the new user state.
initialized.value = false;
await initAuth();
};
const logout = async () => {
try {
await api('/auth/logout', { method: 'POST' });
} finally {
user.value = null;
visitCalled.value = false; // Reset for the next session
await navigateTo('/');
}
};
const updateUser = (partialUser: Partial<User>) => {
if (user.value) {
user.value = { ...user.value, ...partialUser };
}
};
return {
user,
isAuthenticated,
isAnonymous, // Expose this new state
initialized,
initAuth, // Called from app.vue
startOnboarding, // Called from index.vue
register, // Expose register function
login,
logout,
updateUser,
};
}