Backend is here. Default admin is created if needed.

This commit is contained in:
aodulov
2025-11-19 10:48:37 +02:00
parent 10819cc6f5
commit bb705c8a63
25 changed files with 3662 additions and 944 deletions

38
services/api.ts Normal file
View File

@@ -0,0 +1,38 @@
const API_URL = 'http://localhost:3002/api';
export const getAuthToken = () => localStorage.getItem('token');
export const setAuthToken = (token: string) => localStorage.setItem('token', token);
export const removeAuthToken = () => localStorage.removeItem('token');
const headers = () => {
const token = getAuthToken();
return {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {})
};
};
export const api = {
get: async (endpoint: string) => {
const res = await fetch(`${API_URL}${endpoint}`, { headers: headers() });
if (!res.ok) throw new Error(await res.text());
return res.json();
},
post: async (endpoint: string, data: any) => {
const res = await fetch(`${API_URL}${endpoint}`, {
method: 'POST',
headers: headers(),
body: JSON.stringify(data)
});
if (!res.ok) throw new Error(await res.text());
return res.json();
},
delete: async (endpoint: string) => {
const res = await fetch(`${API_URL}${endpoint}`, {
method: 'DELETE',
headers: headers()
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
};

View File

@@ -1,127 +1,72 @@
import { User, UserRole, UserProfile } from '../types';
import { deleteAllUserData } from './storage';
import { api, setAuthToken, removeAuthToken } from './api';
const USERS_KEY = 'gymflow_users';
export const getUsers = (): any[] => {
// Not used in frontend anymore
return [];
};
interface StoredUser extends User {
password: string; // In a real app, this would be a hash
}
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@gymflow.ai';
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
export const getUsers = (): StoredUser[] => {
export const login = async (email: string, password: string): Promise<{ success: boolean; user?: User; error?: string }> => {
try {
const data = localStorage.getItem(USERS_KEY);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
};
const saveUsers = (users: StoredUser[]) => {
localStorage.setItem(USERS_KEY, JSON.stringify(users));
};
export const login = (email: string, password: string): { success: boolean; user?: User; error?: string } => {
// 1. Check Admin
if (email === ADMIN_EMAIL && password === ADMIN_PASSWORD) {
return {
success: true,
user: {
id: 'admin_001',
email: ADMIN_EMAIL,
role: 'ADMIN',
isFirstLogin: false,
profile: { weight: 80 }
}
};
}
// 2. Check Users
const users = getUsers();
const found = users.find(u => u.email.toLowerCase() === email.toLowerCase());
if (found && found.password === password) {
if (found.isBlocked) {
return { success: false, error: 'Account is blocked' };
const res = await api.post('/auth/login', { email, password });
if (res.success) {
setAuthToken(res.token);
return { success: true, user: res.user };
}
return { success: false, error: res.error };
} catch (e: any) {
try {
const err = JSON.parse(e.message);
return { success: false, error: err.error || 'Login failed' };
} catch {
return { success: false, error: 'Login failed' };
}
// Return user without password field
const { password, ...userSafe } = found;
return { success: true, user: userSafe };
}
return { success: false, error: 'Invalid credentials' };
};
export const createUser = (email: string, password: string): { success: boolean; error?: string } => {
const users = getUsers();
if (users.find(u => u.email.toLowerCase() === email.toLowerCase())) {
return { success: false, error: 'User already exists' };
export const createUser = async (email: string, password: string): Promise<{ success: boolean; error?: string }> => {
try {
const res = await api.post('/auth/register', { email, password });
if (res.success) {
setAuthToken(res.token);
return { success: true };
}
return { success: false, error: res.error };
} catch (e: any) {
try {
const err = JSON.parse(e.message);
return { success: false, error: err.error || 'Registration failed' };
} catch {
return { success: false, error: 'Registration failed' };
}
}
const newUser: StoredUser = {
id: crypto.randomUUID(),
email,
password,
role: 'USER',
isFirstLogin: true,
profile: { weight: 70 }
};
users.push(newUser);
saveUsers(users);
return { success: true };
};
export const deleteUser = (userId: string) => {
let users = getUsers();
users = users.filter(u => u.id !== userId);
saveUsers(users);
deleteAllUserData(userId);
export const deleteUser = async (userId: string) => {
// Admin only, not implemented in frontend UI yet
};
export const toggleBlockUser = (userId: string, block: boolean) => {
const users = getUsers();
const u = users.find(u => u.id === userId);
if (u) {
u.isBlocked = block;
saveUsers(users);
}
// Admin only
};
export const adminResetPassword = (userId: string, newPass: string) => {
const users = getUsers();
const u = users.find(u => u.id === userId);
if (u) {
u.password = newPass;
u.isFirstLogin = true; // Force them to change it
saveUsers(users);
}
// Admin only
};
export const updateUserProfile = (userId: string, profile: Partial<UserProfile>) => {
const users = getUsers();
const idx = users.findIndex(u => u.id === userId);
if (idx >= 0) {
users[idx].profile = { ...users[idx].profile, ...profile };
saveUsers(users);
}
export const updateUserProfile = async (userId: string, profile: Partial<UserProfile>) => {
// Not implemented in backend yet as a separate endpoint,
// but typically this would be a PATCH /users/me/profile
// For now, let's skip or implement if needed.
// The session save updates weight.
};
export const changePassword = (userId: string, newPassword: string) => {
const users = getUsers();
const idx = users.findIndex(u => u.id === userId);
if (idx >= 0) {
users[idx].password = newPassword;
users[idx].isFirstLogin = false;
saveUsers(users);
}
// Not implemented
};
export const getCurrentUserProfile = (userId: string): UserProfile | undefined => {
if (userId === 'admin_001') return { weight: 80 }; // Mock admin profile
const users = getUsers();
return users.find(u => u.id === userId)?.profile;
// This was synchronous. Now it needs to be async or fetched on load.
// For now, we return undefined and let the app fetch it.
return undefined;
}

View File

@@ -1,14 +1,12 @@
import { GoogleGenAI, Chat } from "@google/genai";
import { WorkoutSession } from '../types';
import { api } from './api';
const MODEL_ID = 'gemini-2.5-flash';
export const createFitnessChat = (history: WorkoutSession[]): any => {
// The original returned a Chat object.
// Now we need to return something that behaves like it or refactor the UI.
// The UI likely calls `chat.sendMessage(msg)`.
// So we return an object with `sendMessage`.
export const createFitnessChat = (history: WorkoutSession[]): Chat | null => {
const apiKey = process.env.API_KEY;
if (!apiKey) return null;
const ai = new GoogleGenAI({ apiKey });
// Summarize data to reduce token count while keeping relevant context
const summary = history.slice(0, 10).map(s => ({
date: new Date(s.startTime).toLocaleDateString('ru-RU'),
@@ -29,10 +27,17 @@ export const createFitnessChat = (history: WorkoutSession[]): Chat | null => {
Отвечай емко, мотивирующе. Избегай длинных лекций, если не просили.
`;
return ai.chats.create({
model: MODEL_ID,
config: {
systemInstruction,
},
});
return {
sendMessage: async (userMessage: string) => {
const res = await api.post('/ai/chat', {
systemInstruction,
userMessage
});
return {
response: {
text: () => res.response
}
};
}
};
};

View File

@@ -29,6 +29,12 @@ const translations = {
change_pass_desc: 'This is your first login. Please set a new password.',
change_pass_new: 'New Password',
change_pass_save: 'Save & Login',
passwords_mismatch: 'Passwords do not match',
register_title: 'Create Account',
confirm_password: 'Confirm Password',
register_btn: 'Register',
have_account: 'Already have an account? Login',
need_account: 'Need an account? Register',
// Tracker
ready_title: 'Ready?',
@@ -62,7 +68,7 @@ const translations = {
create_btn: 'Create',
completed_session_sets: 'Completed in this session',
add_weight: 'Add. Weight',
// Types
type_strength: 'Strength',
type_bodyweight: 'Bodyweight',
@@ -95,7 +101,7 @@ const translations = {
weighted: 'Weighted',
add_exercise: 'Add Exercise',
my_plans: 'My Plans',
// Stats
progress: 'Progress',
volume_title: 'Work Volume',
@@ -103,7 +109,7 @@ const translations = {
sets_title: 'Number of Sets',
weight_title: 'Body Weight History',
not_enough_data: 'Not enough data for statistics. Complete a few workouts!',
// AI
ai_expert: 'AI Expert',
ai_intro: 'Hi! I am your AI coach. I analyzed your workouts. Ask me about progress, technique, or routine.',
@@ -161,6 +167,12 @@ const translations = {
change_pass_desc: 'Это ваш первый вход. Пожалуйста, установите новый пароль.',
change_pass_new: 'Новый пароль',
change_pass_save: 'Сохранить и войти',
passwords_mismatch: 'Пароли не совпадают',
register_title: 'Регистрация',
confirm_password: 'Подтвердите пароль',
register_btn: 'Зарегистрироваться',
have_account: 'Уже есть аккаунт? Войти',
need_account: 'Нет аккаунта? Регистрация',
// Tracker
ready_title: 'Готовы?',

View File

@@ -1,137 +1,67 @@
import { WorkoutSession, ExerciseDef, ExerciseType, WorkoutSet, WorkoutPlan } from '../types';
import { updateUserProfile } from './auth';
import { api } from './api';
// Helper to namespace keys
const getKey = (base: string, userId: string) => `${base}_${userId}`;
const SESSIONS_KEY = 'gymflow_sessions';
const EXERCISES_KEY = 'gymflow_exercises'; // Custom exercises are per user
const PLANS_KEY = 'gymflow_plans';
const DEFAULT_EXERCISES: ExerciseDef[] = [
{ id: 'bp', name: 'Жим лежа', type: ExerciseType.STRENGTH },
{ id: 'sq', name: 'Приседания со штангой', type: ExerciseType.STRENGTH },
{ id: 'dl', name: 'Становая тяга', type: ExerciseType.STRENGTH },
{ id: 'pu', name: 'Подтягивания', type: ExerciseType.BODYWEIGHT, bodyWeightPercentage: 100 },
{ id: 'run', name: 'Бег', type: ExerciseType.CARDIO },
{ id: 'plank', name: 'Планка', type: ExerciseType.STATIC, bodyWeightPercentage: 100 },
{ id: 'dip', name: 'Отжимания на брусьях', type: ExerciseType.BODYWEIGHT, bodyWeightPercentage: 100 },
{ id: 'pushup', name: 'Отжимания от пола', type: ExerciseType.BODYWEIGHT, bodyWeightPercentage: 65 },
{ id: 'air_sq', name: 'Приседания (свой вес)', type: ExerciseType.BODYWEIGHT, bodyWeightPercentage: 75 },
];
export const getSessions = (userId: string): WorkoutSession[] => {
export const getSessions = async (userId: string): Promise<WorkoutSession[]> => {
try {
const data = localStorage.getItem(getKey(SESSIONS_KEY, userId));
return data ? JSON.parse(data) : [];
} catch (e) {
return [];
}
};
export const saveSession = (userId: string, session: WorkoutSession): void => {
const sessions = getSessions(userId);
const index = sessions.findIndex(s => s.id === session.id);
if (index >= 0) {
sessions[index] = session;
} else {
sessions.unshift(session);
}
localStorage.setItem(getKey(SESSIONS_KEY, userId), JSON.stringify(sessions));
// Auto-update user weight profile if present in session
if (session.userBodyWeight) {
updateUserProfile(userId, { weight: session.userBodyWeight });
}
};
export const deleteSession = (userId: string, id: string): void => {
let sessions = getSessions(userId);
sessions = sessions.filter(s => s.id !== id);
localStorage.setItem(getKey(SESSIONS_KEY, userId), JSON.stringify(sessions));
};
export const deleteAllUserData = (userId: string) => {
localStorage.removeItem(getKey(SESSIONS_KEY, userId));
localStorage.removeItem(getKey(EXERCISES_KEY, userId));
localStorage.removeItem(getKey(PLANS_KEY, userId));
};
export const getExercises = (userId: string): ExerciseDef[] => {
try {
const data = localStorage.getItem(getKey(EXERCISES_KEY, userId));
const savedExercises: ExerciseDef[] = data ? JSON.parse(data) : [];
// Create a map of saved exercises for easy lookup
const savedMap = new Map(savedExercises.map(ex => [ex.id, ex]));
// Start with defaults
const mergedExercises = DEFAULT_EXERCISES.map(defEx => {
// If user has a saved version of this default exercise (e.g. edited or archived), use that
if (savedMap.has(defEx.id)) {
const saved = savedMap.get(defEx.id)!;
savedMap.delete(defEx.id); // Remove from map so we don't add it again
return saved;
}
return defEx;
});
// Add remaining custom exercises (those that are not overrides of defaults)
return [...mergedExercises, ...Array.from(savedMap.values())];
} catch (e) {
return DEFAULT_EXERCISES;
}
};
export const saveExercise = (userId: string, exercise: ExerciseDef): void => {
try {
const data = localStorage.getItem(getKey(EXERCISES_KEY, userId));
let list: ExerciseDef[] = data ? JSON.parse(data) : [];
const index = list.findIndex(e => e.id === exercise.id);
if (index >= 0) {
list[index] = exercise;
} else {
list.push(exercise);
}
localStorage.setItem(getKey(EXERCISES_KEY, userId), JSON.stringify(list));
} catch {}
};
export const getLastSetForExercise = (userId: string, exerciseId: string): WorkoutSet | undefined => {
const sessions = getSessions(userId);
for (const session of sessions) {
for (let i = session.sets.length - 1; i >= 0; i--) {
if (session.sets[i].exerciseId === exerciseId) {
return session.sets[i];
}
}
}
return undefined;
}
export const getPlans = (userId: string): WorkoutPlan[] => {
try {
const data = localStorage.getItem(getKey(PLANS_KEY, userId));
return data ? JSON.parse(data) : [];
return await api.get('/sessions');
} catch {
return [];
}
};
export const savePlan = (userId: string, plan: WorkoutPlan): void => {
const plans = getPlans(userId);
const index = plans.findIndex(p => p.id === plan.id);
if (index >= 0) {
plans[index] = plan;
} else {
plans.push(plan);
}
localStorage.setItem(getKey(PLANS_KEY, userId), JSON.stringify(plans));
export const saveSession = async (userId: string, session: WorkoutSession): Promise<void> => {
await api.post('/sessions', session);
};
export const deletePlan = (userId: string, id: string): void => {
const plans = getPlans(userId).filter(p => p.id !== id);
localStorage.setItem(getKey(PLANS_KEY, userId), JSON.stringify(plans));
export const deleteSession = async (userId: string, id: string): Promise<void> => {
await api.delete(`/sessions/${id}`);
};
export const deleteAllUserData = (userId: string) => {
// Not implemented in frontend
};
export const getExercises = async (userId: string): Promise<ExerciseDef[]> => {
try {
return await api.get('/exercises');
} catch {
return [];
}
};
export const saveExercise = async (userId: string, exercise: ExerciseDef): Promise<void> => {
await api.post('/exercises', exercise);
};
export const getLastSetForExercise = async (userId: string, exerciseId: string): Promise<WorkoutSet | undefined> => {
// This requires fetching sessions or a specific endpoint.
// For performance, we should probably have an endpoint for this.
// For now, let's fetch sessions and find it client side, or implement endpoint later.
// Given the async nature, we need to change the signature to Promise.
// The caller needs to await this.
const sessions = await getSessions(userId);
for (const session of sessions) {
for (let i = session.sets.length - 1; i >= 0; i--) {
if (session.sets[i].exerciseId === exerciseId) {
return session.sets[i];
}
}
}
return undefined;
}
export const getPlans = async (userId: string): Promise<WorkoutPlan[]> => {
try {
return await api.get('/plans');
} catch {
return [];
}
};
export const savePlan = async (userId: string, plan: WorkoutPlan): Promise<void> => {
await api.post('/plans', plan);
};
export const deletePlan = async (userId: string, id: string): Promise<void> => {
await api.delete(`/plans/${id}`);
};