progress
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
'is_trainer' => ['nullable', 'boolean'],
|
||||
'age' => ['nullable', 'integer', 'min:5', 'max:120'],
|
||||
'weight_kg' => ['nullable', 'numeric', 'min:20', 'max:400'],
|
||||
'height_cm' => ['nullable', 'numeric', 'min:80', 'max:250'],
|
||||
'fitness_level' => ['nullable', 'in:beginner,intermediate,advanced'],
|
||||
'goal' => ['nullable', 'string', 'max:50'],
|
||||
'health_notes' => ['nullable', 'string', 'max:65535'],
|
||||
'available_equipment' => ['nullable', 'string', 'max:65535'],
|
||||
'days_per_week' => ['nullable', 'integer', 'min:1', 'max:7'],
|
||||
'minutes_per_session' => ['nullable', 'integer', 'min:5', 'max:600'],
|
||||
'device_name' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'is_trainer' => (bool) ($data['is_trainer'] ?? false),
|
||||
'age' => $data['age'] ?? null,
|
||||
'weight_kg' => $data['weight_kg'] ?? null,
|
||||
'height_cm' => $data['height_cm'] ?? null,
|
||||
'fitness_level' => $data['fitness_level'] ?? null,
|
||||
'goal' => $data['goal'] ?? null,
|
||||
'health_notes' => $data['health_notes'] ?? null,
|
||||
'available_equipment' => $data['available_equipment'] ?? null,
|
||||
'days_per_week' => $data['days_per_week'] ?? null,
|
||||
'minutes_per_session' => $data['minutes_per_session'] ?? null,
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
$token = $user->createToken($data['device_name'])->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'user' => new UserResource($user),
|
||||
'token' => $token,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
'device_name' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
if (!$user || !Hash::check($data['password'], $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => ['The provided credentials are incorrect.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$token = $user->createToken($data['device_name'])->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'user' => new UserResource($user),
|
||||
'token' => $token,
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->currentAccessToken()->delete();
|
||||
|
||||
return response()->json(['message' => 'Logged out']);
|
||||
}
|
||||
|
||||
public function me(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json(['user' => new UserResource($request->user())]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\WeightLogResource;
|
||||
use App\Models\WorkoutSession;
|
||||
use App\Support\Gamification;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$workoutsCount = $user->workouts()->count();
|
||||
$sessionsQuery = WorkoutSession::where('user_id', $user->id);
|
||||
$sessionsCount = (clone $sessionsQuery)->count();
|
||||
$completedSessions = (clone $sessionsQuery)->whereNotNull('completed_at')->count();
|
||||
$completionRate = $sessionsCount > 0 ? round(($completedSessions / $sessionsCount) * 100) : 0;
|
||||
|
||||
$recentSessions = (clone $sessionsQuery)
|
||||
->latest('started_at')
|
||||
->latest('id')
|
||||
->with(['workout:id,title', 'items:id,session_id,done'])
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(function ($s) {
|
||||
$total = $s->items->count();
|
||||
$done = $s->items->where('done', true)->count();
|
||||
$pct = $total > 0 ? (int) round(($done / $total) * 100) : 0;
|
||||
|
||||
return [
|
||||
'id' => $s->id,
|
||||
'workout_title' => optional($s->workout)->title ?? 'Workout',
|
||||
'started_at' => $s->started_at,
|
||||
'completed_at' => $s->completed_at,
|
||||
'done' => $done,
|
||||
'total' => $total,
|
||||
'pct' => $pct,
|
||||
];
|
||||
});
|
||||
|
||||
$since = now()->subDays(29)->startOfDay();
|
||||
$dailyActivity = (clone $sessionsQuery)
|
||||
->where('created_at', '>=', $since)
|
||||
->get(['id', 'created_at'])
|
||||
->groupBy(fn ($s) => $s->created_at->format('Y-m-d'))
|
||||
->map->count();
|
||||
|
||||
$weightLogs = $user->weightLogs()->orderBy('logged_at')->get(['id', 'weight_kg', 'logged_at']);
|
||||
$weightChange = $weightLogs->count() >= 2
|
||||
? round($weightLogs->last()->weight_kg - $weightLogs->first()->weight_kg, 2)
|
||||
: null;
|
||||
|
||||
$gamification = Gamification::forUser($user);
|
||||
|
||||
return response()->json([
|
||||
'workouts_count' => $workoutsCount,
|
||||
'sessions_count' => $sessionsCount,
|
||||
'completed_sessions' => $completedSessions,
|
||||
'completion_rate' => $completionRate,
|
||||
'recent_sessions' => $recentSessions,
|
||||
'daily_activity' => $dailyActivity,
|
||||
'weight_logs' => WeightLogResource::collection($weightLogs),
|
||||
'weight_change' => $weightChange,
|
||||
'gamification' => $gamification,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DeviceController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$currentTokenId = $request->user()->currentAccessToken()?->id;
|
||||
|
||||
$tokens = $request->user()->tokens()->orderByDesc('last_used_at')->get()->map(fn ($token) => [
|
||||
'id' => $token->id,
|
||||
'name' => $token->name,
|
||||
'is_current_device' => $token->id === $currentTokenId,
|
||||
'last_used_at' => $token->last_used_at,
|
||||
'created_at' => $token->created_at,
|
||||
]);
|
||||
|
||||
return response()->json(['data' => $tokens]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, int $tokenId): JsonResponse
|
||||
{
|
||||
$deleted = $request->user()->tokens()->where('id', $tokenId)->delete();
|
||||
|
||||
if (!$deleted) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Device signed out']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailVerificationController extends Controller
|
||||
{
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return response()->json(['message' => 'Email already verified.']);
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
return response()->json(['message' => 'Verification link sent.']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Exercise;
|
||||
use App\Models\ExerciseAlias;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ExerciseAliasController extends Controller
|
||||
{
|
||||
public function store(Request $request, Exercise $exercise): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$name = trim((string) ($data['name'] ?? ''));
|
||||
$userId = Auth::id();
|
||||
|
||||
if ($name === '') {
|
||||
ExerciseAlias::where('user_id', $userId)->where('exercise_id', $exercise->id)->delete();
|
||||
|
||||
return response()->json(['message' => __('app.exercises.rename_cleared'), 'name' => null]);
|
||||
}
|
||||
|
||||
ExerciseAlias::updateOrCreate(
|
||||
['user_id' => $userId, 'exercise_id' => $exercise->id],
|
||||
['name' => $name]
|
||||
);
|
||||
|
||||
return response()->json(['message' => __('app.exercises.renamed_success'), 'name' => $name]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ExerciseResource;
|
||||
use App\Models\BodyPart;
|
||||
use App\Models\Equipment;
|
||||
use App\Models\Exercise;
|
||||
use App\Models\Muscle;
|
||||
use App\Models\WorkoutSessionItem;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ExerciseController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
$bodypart = (string) $request->query('bodypart', '');
|
||||
$muscle = (string) $request->query('muscle', '');
|
||||
$equipment = (string) $request->query('equipment', '');
|
||||
$perPage = (int) ($request->query('per_page', 12));
|
||||
if ($perPage < 6 || $perPage > 60) { $perPage = 12; }
|
||||
|
||||
$query = Exercise::query()
|
||||
->with(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'secondaryMuscles:id,name,slug', 'equipments:id,name,slug']
|
||||
+ (Auth::check() ? ['aliases' => function ($rel) { $rel->where('user_id', Auth::id()); }] : []))
|
||||
->when($q !== '', function ($qBuilder) use ($q) {
|
||||
$qBuilder->where(function ($sub) use ($q) {
|
||||
$sub->where('name', 'like', "%{$q}%")
|
||||
->orWhere('instructions', 'like', "%{$q}%");
|
||||
});
|
||||
})
|
||||
->when($bodypart !== '', function ($qBuilder) use ($bodypart) {
|
||||
$qBuilder->whereHas('bodyParts', function ($rel) use ($bodypart) {
|
||||
$rel->where('slug', $bodypart);
|
||||
});
|
||||
})
|
||||
->when($muscle !== '', function ($qBuilder) use ($muscle) {
|
||||
$qBuilder->where(function ($sub) use ($muscle) {
|
||||
$sub->whereHas('targetMuscles', function ($rel) use ($muscle) { $rel->where('slug', $muscle); })
|
||||
->orWhereHas('secondaryMuscles', function ($rel) use ($muscle) { $rel->where('slug', $muscle); });
|
||||
});
|
||||
})
|
||||
->when($equipment !== '', function ($qBuilder) use ($equipment) {
|
||||
$qBuilder->whereHas('equipments', function ($rel) use ($equipment) {
|
||||
$rel->where('slug', $equipment);
|
||||
});
|
||||
})
|
||||
->orderBy('name');
|
||||
|
||||
$exercises = $query->paginate($perPage)->withQueryString();
|
||||
|
||||
return response()->json([
|
||||
'data' => ExerciseResource::collection($exercises->items()),
|
||||
'meta' => [
|
||||
'current_page' => $exercises->currentPage(),
|
||||
'last_page' => $exercises->lastPage(),
|
||||
'per_page' => $exercises->perPage(),
|
||||
'total' => $exercises->total(),
|
||||
],
|
||||
'filters' => [
|
||||
'body_parts' => BodyPart::orderBy('name')->get(['id', 'name', 'slug']),
|
||||
'muscles' => Muscle::orderBy('name')->get(['id', 'name', 'slug']),
|
||||
'equipments' => Equipment::orderBy('name')->get(['id', 'name', 'slug']),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Exercise $exercise): JsonResponse
|
||||
{
|
||||
$exercise->load(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'secondaryMuscles:id,name,slug', 'equipments:id,name,slug']
|
||||
+ (Auth::check() ? ['aliases' => function ($rel) { $rel->where('user_id', Auth::id()); }] : []));
|
||||
|
||||
$targetIds = $exercise->targetMuscles->pluck('id')->all();
|
||||
$bodyPartIds = $exercise->bodyParts->pluck('id')->all();
|
||||
$equipmentIds = $exercise->equipments->pluck('id')->all();
|
||||
|
||||
$similarExercises = Exercise::query()
|
||||
->with(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'equipments:id,name,slug']
|
||||
+ (Auth::check() ? ['aliases' => function ($rel) { $rel->where('user_id', Auth::id()); }] : []))
|
||||
->where('id', '!=', $exercise->id)
|
||||
->where(function ($q) use ($targetIds, $bodyPartIds, $equipmentIds) {
|
||||
if (!empty($targetIds)) {
|
||||
$q->orWhereHas('targetMuscles', fn ($rel) => $rel->whereIn('id', $targetIds));
|
||||
}
|
||||
if (!empty($bodyPartIds)) {
|
||||
$q->orWhereHas('bodyParts', fn ($rel) => $rel->whereIn('id', $bodyPartIds));
|
||||
}
|
||||
if (!empty($equipmentIds)) {
|
||||
$q->orWhereHas('equipments', fn ($rel) => $rel->whereIn('id', $equipmentIds));
|
||||
}
|
||||
})
|
||||
->orderBy('name')
|
||||
->limit(9)
|
||||
->get();
|
||||
|
||||
$personalBest = null;
|
||||
if (Auth::check()) {
|
||||
$personalBest = WorkoutSessionItem::query()
|
||||
->whereHas('workoutExercise', fn ($q) => $q->where('exercise_id', $exercise->id))
|
||||
->whereHas('session', fn ($q) => $q->where('user_id', Auth::id()))
|
||||
->where('done', true)
|
||||
->whereNotNull('actual_weight')
|
||||
->orderByDesc('actual_weight')
|
||||
->orderByDesc('actual_reps')
|
||||
->first();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'exercise' => new ExerciseResource($exercise),
|
||||
'similar_exercises' => ExerciseResource::collection($similarExercises),
|
||||
'personal_best' => $personalBest ? [
|
||||
'actual_weight' => $personalBest->actual_weight !== null ? (float) $personalBest->actual_weight : null,
|
||||
'actual_reps' => $personalBest->actual_reps,
|
||||
] : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Exercise;
|
||||
use App\Models\Workout;
|
||||
use App\Models\YogaPose;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class FavoriteController extends Controller
|
||||
{
|
||||
public function toggleExercise(Exercise $exercise): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
$exists = $user->favoriteExercises()->where('exercise_id', $exercise->id)->exists();
|
||||
if ($exists) {
|
||||
$user->favoriteExercises()->detach($exercise->id);
|
||||
} else {
|
||||
$user->favoriteExercises()->attach($exercise->id);
|
||||
}
|
||||
|
||||
return response()->json(['favorited' => !$exists]);
|
||||
}
|
||||
|
||||
public function toggleWorkout(Workout $workout): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
$exists = $user->favoriteWorkouts()->where('workout_id', $workout->id)->exists();
|
||||
if ($exists) {
|
||||
$user->favoriteWorkouts()->detach($workout->id);
|
||||
} else {
|
||||
$user->favoriteWorkouts()->attach($workout->id);
|
||||
}
|
||||
|
||||
return response()->json(['favorited' => !$exists]);
|
||||
}
|
||||
|
||||
public function toggleYogaPose(YogaPose $pose): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
$exists = $user->favoriteYogaPoses()->where('yoga_pose_id', $pose->id)->exists();
|
||||
if ($exists) {
|
||||
$user->favoriteYogaPoses()->detach($pose->id);
|
||||
} else {
|
||||
$user->favoriteYogaPoses()->attach($pose->id);
|
||||
}
|
||||
|
||||
return response()->json(['favorited' => !$exists]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
|
||||
class PasswordResetController extends Controller
|
||||
{
|
||||
public function forgotPassword(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
]);
|
||||
|
||||
$status = Password::sendResetLink($request->only('email'));
|
||||
|
||||
return $status === Password::RESET_LINK_SENT
|
||||
? response()->json(['message' => __($status)])
|
||||
: response()->json(['message' => __($status)], 422);
|
||||
}
|
||||
|
||||
public function resetPassword(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function (User $user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
// A password reset often means "I lost my phone" - revoke every
|
||||
// existing API token rather than leaving old devices signed in.
|
||||
$user->tokens()->delete();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
return $status === Password::PASSWORD_RESET
|
||||
? response()->json(['message' => __($status)])
|
||||
: response()->json(['message' => __($status)], 422);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ProfileUpdateRequest;
|
||||
use App\Http\Resources\UserResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function show(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json(['user' => new UserResource($request->user())]);
|
||||
}
|
||||
|
||||
public function update(ProfileUpdateRequest $request): JsonResponse
|
||||
{
|
||||
$request->user()->fill($request->validated());
|
||||
|
||||
if ($request->user()->isDirty('email')) {
|
||||
$request->user()->email_verified_at = null;
|
||||
}
|
||||
|
||||
$request->user()->save();
|
||||
|
||||
return response()->json(['user' => new UserResource($request->user())]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$user->tokens()->delete();
|
||||
$user->delete();
|
||||
|
||||
return response()->json(['message' => 'Account deleted']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ProgramResource;
|
||||
use App\Models\Program;
|
||||
use App\Models\WorkoutSession;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ProgramController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$programs = Auth::user()->programs()->withCount('workouts')->latest()->paginate(10);
|
||||
|
||||
return response()->json([
|
||||
'data' => ProgramResource::collection($programs->items()),
|
||||
'meta' => [
|
||||
'current_page' => $programs->currentPage(),
|
||||
'last_page' => $programs->lastPage(),
|
||||
'per_page' => $programs->perPage(),
|
||||
'total' => $programs->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'weeks' => ['required', 'integer', 'min:1', 'max:52'],
|
||||
]);
|
||||
|
||||
$program = Auth::user()->programs()->create($data);
|
||||
|
||||
return response()->json(['program' => new ProgramResource($program)], 201);
|
||||
}
|
||||
|
||||
public function show(Program $program): JsonResponse
|
||||
{
|
||||
$this->authorizeProgram($program);
|
||||
$program->load('workouts');
|
||||
|
||||
$completedWorkoutIds = WorkoutSession::query()
|
||||
->where('user_id', Auth::id())
|
||||
->whereIn('workout_id', $program->workouts->pluck('id'))
|
||||
->whereNotNull('completed_at')
|
||||
->pluck('workout_id')
|
||||
->unique();
|
||||
|
||||
$totalWorkouts = $program->workouts->count();
|
||||
$completedCount = $completedWorkoutIds->count();
|
||||
$progressPct = $totalWorkouts > 0 ? (int) round(($completedCount / $totalWorkouts) * 100) : 0;
|
||||
|
||||
return response()->json([
|
||||
'program' => new ProgramResource($program),
|
||||
'completed_workout_ids' => $completedWorkoutIds->values(),
|
||||
'progress_pct' => $progressPct,
|
||||
'completed_count' => $completedCount,
|
||||
'total_workouts' => $totalWorkouts,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Program $program): JsonResponse
|
||||
{
|
||||
$this->authorizeProgram($program);
|
||||
$program->delete();
|
||||
|
||||
return response()->json(['message' => 'Program deleted']);
|
||||
}
|
||||
|
||||
public function addWorkout(Request $request, Program $program): JsonResponse
|
||||
{
|
||||
$this->authorizeProgram($program);
|
||||
$data = $request->validate([
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'week_number' => ['required', 'integer', 'min:1', 'max:'.$program->weeks],
|
||||
'day_number' => ['required', 'integer', 'min:1', 'max:14'],
|
||||
'estimated_minutes' => ['nullable', 'integer', 'min:1', 'max:600'],
|
||||
]);
|
||||
|
||||
$workout = $program->workouts()->create([
|
||||
'user_id' => Auth::id(),
|
||||
'title' => $data['title'],
|
||||
'estimated_minutes' => $data['estimated_minutes'] ?? null,
|
||||
'week_number' => $data['week_number'],
|
||||
'day_number' => $data['day_number'],
|
||||
]);
|
||||
|
||||
return response()->json(['workout_id' => $workout->id, 'message' => 'Workout added to program'], 201);
|
||||
}
|
||||
|
||||
protected function authorizeProgram(Program $program): void
|
||||
{
|
||||
if ($program->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Http\Resources\WorkoutResource;
|
||||
use App\Models\User;
|
||||
use App\Models\Workout;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TrainerController extends Controller
|
||||
{
|
||||
public function clients(): JsonResponse
|
||||
{
|
||||
$trainer = Auth::user();
|
||||
$clients = $trainer->clients()
|
||||
->with(['workouts' => function ($q) use ($trainer) {
|
||||
$q->where('assigned_by_trainer_id', $trainer->id)->latest();
|
||||
}])
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'data' => $clients->map(fn ($client) => [
|
||||
'client' => new UserResource($client),
|
||||
'assigned_workouts' => WorkoutResource::collection($client->workouts),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function addClient(Request $request): JsonResponse
|
||||
{
|
||||
$trainer = Auth::user();
|
||||
$data = $request->validate([
|
||||
'email' => ['required', 'email', 'exists:users,email'],
|
||||
]);
|
||||
|
||||
$client = User::where('email', $data['email'])->firstOrFail();
|
||||
|
||||
if ($client->id === $trainer->id) {
|
||||
return response()->json(['message' => 'You cannot add yourself as a client.'], 422);
|
||||
}
|
||||
|
||||
if ($trainer->clients()->where('client_id', $client->id)->exists()) {
|
||||
return response()->json(['message' => 'This person is already one of your clients.'], 422);
|
||||
}
|
||||
|
||||
$trainer->clients()->attach($client->id);
|
||||
|
||||
return response()->json(['message' => 'Client added', 'client' => new UserResource($client)], 201);
|
||||
}
|
||||
|
||||
public function removeClient(User $client): JsonResponse
|
||||
{
|
||||
Auth::user()->clients()->detach($client->id);
|
||||
|
||||
return response()->json(['message' => 'Client removed']);
|
||||
}
|
||||
|
||||
public function storeWorkout(Request $request, User $client): JsonResponse
|
||||
{
|
||||
$this->authorizeClient($client);
|
||||
|
||||
$data = $request->validate([
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'notes' => ['nullable', 'string'],
|
||||
'estimated_minutes' => ['nullable', 'integer', 'min:1', 'max:600'],
|
||||
]);
|
||||
|
||||
$workout = Workout::create([
|
||||
'user_id' => $client->id,
|
||||
'assigned_by_trainer_id' => Auth::id(),
|
||||
'title' => $data['title'],
|
||||
'notes' => $data['notes'] ?? null,
|
||||
'estimated_minutes' => $data['estimated_minutes'] ?? null,
|
||||
]);
|
||||
|
||||
return response()->json(['workout' => new WorkoutResource($workout)], 201);
|
||||
}
|
||||
|
||||
protected function authorizeClient(User $client): void
|
||||
{
|
||||
if (!Auth::user()->clients()->where('client_id', $client->id)->exists()) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\WeightLogResource;
|
||||
use App\Models\WeightLog;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class WeightLogController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$logs = Auth::user()->weightLogs()->orderBy('logged_at')->get();
|
||||
|
||||
return response()->json(['data' => WeightLogResource::collection($logs)]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'weight_kg' => ['required', 'numeric', 'min:20', 'max:400'],
|
||||
'logged_at' => ['nullable', 'date', 'before_or_equal:today'],
|
||||
]);
|
||||
|
||||
$loggedAt = $data['logged_at'] ?? now()->toDateString();
|
||||
|
||||
$entry = WeightLog::where('user_id', Auth::id())->whereDate('logged_at', $loggedAt)->first()
|
||||
?? new WeightLog(['user_id' => Auth::id(), 'logged_at' => $loggedAt]);
|
||||
$entry->weight_kg = $data['weight_kg'];
|
||||
$entry->save();
|
||||
|
||||
return response()->json(['weight_log' => new WeightLogResource($entry)], 201);
|
||||
}
|
||||
|
||||
public function destroy(WeightLog $weightLog): JsonResponse
|
||||
{
|
||||
if ($weightLog->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$weightLog->delete();
|
||||
|
||||
return response()->json(['message' => __('app.weight_log.deleted')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ExerciseResource;
|
||||
use App\Http\Resources\WorkoutExerciseResource;
|
||||
use App\Http\Resources\WorkoutResource;
|
||||
use App\Models\Exercise;
|
||||
use App\Models\Workout;
|
||||
use App\Models\WorkoutExercise;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class WorkoutController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$workouts = Auth::user()->workouts()->with('assignedByTrainer')->latest()->paginate(10);
|
||||
|
||||
return $this->paginated($workouts);
|
||||
}
|
||||
|
||||
public function community(Request $request): JsonResponse
|
||||
{
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
|
||||
$workouts = Workout::query()
|
||||
->where('is_public', true)
|
||||
->with(['user:id,name', 'items'])
|
||||
->when($q !== '', fn ($qb) => $qb->where('title', 'like', "%{$q}%"))
|
||||
->latest()
|
||||
->paginate(12)
|
||||
->withQueryString();
|
||||
|
||||
return $this->paginated($workouts);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'notes' => 'nullable|string',
|
||||
'estimated_minutes' => 'nullable|integer|min:1|max:600',
|
||||
]);
|
||||
$workout = Auth::user()->workouts()->create($data);
|
||||
|
||||
return response()->json(['workout' => new WorkoutResource($workout)], 201);
|
||||
}
|
||||
|
||||
public function show(Workout $workout): JsonResponse
|
||||
{
|
||||
$this->authorizeView($workout);
|
||||
$workout->load(['items.exercise', 'assignedByTrainer', 'user:id,name']);
|
||||
|
||||
return response()->json(['workout' => new WorkoutResource($workout)]);
|
||||
}
|
||||
|
||||
public function searchExercises(Request $request, Workout $workout): JsonResponse
|
||||
{
|
||||
$this->authorizeWorkout($workout);
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
|
||||
$exercises = Exercise::query()
|
||||
->with(['aliases' => fn ($rel) => $rel->where('user_id', Auth::id())])
|
||||
->when($q !== '', fn ($qb) => $qb->where('name', 'like', "%{$q}%"))
|
||||
->orderBy('name')
|
||||
->limit(20)
|
||||
->get(['id', 'name']);
|
||||
|
||||
return response()->json(ExerciseResource::collection($exercises));
|
||||
}
|
||||
|
||||
public function togglePublic(Workout $workout): JsonResponse
|
||||
{
|
||||
if ($workout->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
$workout->is_public = !$workout->is_public;
|
||||
$workout->save();
|
||||
|
||||
return response()->json(['workout' => new WorkoutResource($workout)]);
|
||||
}
|
||||
|
||||
public function copy(Workout $workout): JsonResponse
|
||||
{
|
||||
if (!$workout->is_public && $workout->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$workout->loadMissing('items');
|
||||
|
||||
$copy = Auth::user()->workouts()->create([
|
||||
'title' => $workout->title.' (copy)',
|
||||
'notes' => $workout->notes,
|
||||
'estimated_minutes' => $workout->estimated_minutes,
|
||||
]);
|
||||
|
||||
foreach ($workout->items as $item) {
|
||||
$copy->items()->create([
|
||||
'exercise_id' => $item->exercise_id,
|
||||
'position' => $item->position,
|
||||
'sets' => $item->sets,
|
||||
'reps' => $item->reps,
|
||||
'weight' => $item->weight,
|
||||
'rest_seconds' => $item->rest_seconds,
|
||||
'notes' => $item->notes,
|
||||
]);
|
||||
}
|
||||
|
||||
$copy->load(['items.exercise']);
|
||||
|
||||
return response()->json(['workout' => new WorkoutResource($copy)], 201);
|
||||
}
|
||||
|
||||
public function addExercise(Request $request, Workout $workout): JsonResponse
|
||||
{
|
||||
$this->authorizeWorkout($workout);
|
||||
$data = $request->validate([
|
||||
'exercise_id' => 'required|exists:exercises,id',
|
||||
'position' => 'nullable|integer|min:1',
|
||||
'sets' => 'nullable|integer|min:1|max:20',
|
||||
'reps' => 'nullable|integer|min:1|max:200',
|
||||
'weight' => 'nullable|numeric|min:0|max:999.99',
|
||||
'rest_seconds' => 'nullable|integer|min:0|max:600',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
$nextPos = ($workout->items()->max('position') ?? 0) + 1;
|
||||
$data['position'] = $data['position'] ?? $nextPos;
|
||||
$item = $workout->items()->create($data);
|
||||
$item->load('exercise');
|
||||
|
||||
return response()->json(['item' => new WorkoutExerciseResource($item)], 201);
|
||||
}
|
||||
|
||||
public function updateExercise(Request $request, Workout $workout, WorkoutExercise $item): JsonResponse
|
||||
{
|
||||
$this->authorizeWorkout($workout);
|
||||
if ($item->workout_id !== $workout->id) {
|
||||
abort(404);
|
||||
}
|
||||
$data = $request->validate([
|
||||
'position' => 'nullable|integer|min:1',
|
||||
'sets' => 'nullable|integer|min:1|max:20',
|
||||
'reps' => 'nullable|integer|min:1|max:200',
|
||||
'weight' => 'nullable|numeric|min:0|max:999.99',
|
||||
'rest_seconds' => 'nullable|integer|min:0|max:600',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
$item->update($data);
|
||||
$item->load('exercise');
|
||||
|
||||
return response()->json(['item' => new WorkoutExerciseResource($item)]);
|
||||
}
|
||||
|
||||
public function removeExercise(Workout $workout, WorkoutExercise $item): JsonResponse
|
||||
{
|
||||
$this->authorizeWorkout($workout);
|
||||
if ($item->workout_id !== $workout->id) {
|
||||
abort(404);
|
||||
}
|
||||
$item->delete();
|
||||
|
||||
return response()->json(['message' => 'Exercise removed']);
|
||||
}
|
||||
|
||||
public function destroy(Workout $workout): JsonResponse
|
||||
{
|
||||
if ($workout->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
$workout->delete();
|
||||
|
||||
return response()->json(['message' => 'Workout deleted']);
|
||||
}
|
||||
|
||||
protected function authorizeWorkout(Workout $workout): void
|
||||
{
|
||||
if ($workout->user_id === Auth::id()) {
|
||||
return;
|
||||
}
|
||||
if ($workout->assigned_by_trainer_id && $workout->assigned_by_trainer_id === Auth::id()) {
|
||||
return;
|
||||
}
|
||||
abort(403);
|
||||
}
|
||||
|
||||
protected function authorizeView(Workout $workout): void
|
||||
{
|
||||
if ($workout->is_public) {
|
||||
return;
|
||||
}
|
||||
$this->authorizeWorkout($workout);
|
||||
}
|
||||
|
||||
protected function paginated($paginator): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => WorkoutResource::collection($paginator->items()),
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\WorkoutSessionResource;
|
||||
use App\Models\Workout;
|
||||
use App\Models\WorkoutExercise;
|
||||
use App\Models\WorkoutSession;
|
||||
use App\Models\WorkoutSessionItem;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class WorkoutSessionController extends Controller
|
||||
{
|
||||
public function store(Request $request, Workout $workout): JsonResponse
|
||||
{
|
||||
$this->authorizeWorkout($workout);
|
||||
|
||||
$session = WorkoutSession::create([
|
||||
'user_id' => Auth::id(),
|
||||
'workout_id' => $workout->id,
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
$workout->loadMissing('items');
|
||||
foreach ($workout->items as $item) {
|
||||
WorkoutSessionItem::create([
|
||||
'session_id' => $session->id,
|
||||
'workout_exercise_id' => $item->id,
|
||||
'done' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$session->load(['workout.items.exercise', 'items']);
|
||||
|
||||
return response()->json(['session' => new WorkoutSessionResource($session)], 201);
|
||||
}
|
||||
|
||||
public function show(WorkoutSession $session): JsonResponse
|
||||
{
|
||||
$this->authorizeSession($session);
|
||||
$session->load(['workout.items.exercise', 'items']);
|
||||
|
||||
return response()->json(['session' => new WorkoutSessionResource($session)]);
|
||||
}
|
||||
|
||||
public function toggleItem(Request $request, WorkoutSession $session, WorkoutExercise $item): JsonResponse
|
||||
{
|
||||
$this->authorizeSession($session);
|
||||
if ($item->workout_id !== $session->workout_id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'actual_weight' => ['nullable', 'numeric', 'min:0', 'max:9999.99'],
|
||||
'actual_reps' => ['nullable', 'integer', 'min:0', 'max:1000'],
|
||||
]);
|
||||
|
||||
$si = WorkoutSessionItem::firstOrCreate([
|
||||
'session_id' => $session->id,
|
||||
'workout_exercise_id' => $item->id,
|
||||
], [
|
||||
'done' => false,
|
||||
]);
|
||||
|
||||
$si->done = !$si->done;
|
||||
if ($si->done) {
|
||||
if (array_key_exists('actual_weight', $data) && $data['actual_weight'] !== null) {
|
||||
$si->actual_weight = $data['actual_weight'];
|
||||
}
|
||||
if (array_key_exists('actual_reps', $data) && $data['actual_reps'] !== null) {
|
||||
$si->actual_reps = $data['actual_reps'];
|
||||
}
|
||||
}
|
||||
$si->save();
|
||||
|
||||
return response()->json(['item' => $si]);
|
||||
}
|
||||
|
||||
public function complete(WorkoutSession $session): JsonResponse
|
||||
{
|
||||
$this->authorizeSession($session);
|
||||
$session->completed_at = now();
|
||||
$session->save();
|
||||
$session->load(['workout.items.exercise', 'items']);
|
||||
|
||||
return response()->json(['session' => new WorkoutSessionResource($session)]);
|
||||
}
|
||||
|
||||
protected function authorizeWorkout(Workout $workout): void
|
||||
{
|
||||
if ($workout->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
protected function authorizeSession(WorkoutSession $session): void
|
||||
{
|
||||
if ($session->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\YogaPoseResource;
|
||||
use App\Models\YogaCategory;
|
||||
use App\Models\YogaPose;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class YogaPoseController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
$category = (int) $request->query('category', 0);
|
||||
$benefit = trim((string) $request->query('benefit', ''));
|
||||
$perPage = (int) $request->query('per_page', 12);
|
||||
if ($perPage < 6 || $perPage > 60) { $perPage = 12; }
|
||||
|
||||
$query = YogaPose::query()->with('category')
|
||||
->when($q !== '', function ($qb) use ($q) {
|
||||
$qb->where(function ($sub) use ($q) {
|
||||
$sub->where('english_name', 'like', "%{$q}%")
|
||||
->orWhere('sanskrit_name', 'like', "%{$q}%")
|
||||
->orWhere('sanskrit_name_adapted', 'like', "%{$q}%");
|
||||
});
|
||||
})
|
||||
->when($category > 0, fn ($qb) => $qb->where('category_id', $category))
|
||||
->when($benefit !== '', fn ($qb) => $qb->where('benefits', 'like', "%{$benefit}%"))
|
||||
->orderBy('english_name');
|
||||
|
||||
$poses = $query->paginate($perPage)->withQueryString();
|
||||
|
||||
return response()->json([
|
||||
'data' => YogaPoseResource::collection($poses->items()),
|
||||
'meta' => [
|
||||
'current_page' => $poses->currentPage(),
|
||||
'last_page' => $poses->lastPage(),
|
||||
'per_page' => $poses->perPage(),
|
||||
'total' => $poses->total(),
|
||||
],
|
||||
'categories' => YogaCategory::orderBy('name')->get(['id', 'name']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(YogaPose $pose): JsonResponse
|
||||
{
|
||||
$pose->load('category');
|
||||
|
||||
$benefits = (string) $pose->benefits;
|
||||
$tokens = collect(preg_split('/[^\p{L}]+/u', mb_strtolower($benefits)))
|
||||
->filter(fn ($t) => $t !== null && $t !== '' && mb_strlen($t) >= 4)
|
||||
->unique()
|
||||
->take(8)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$similarPoses = YogaPose::query()
|
||||
->with('category')
|
||||
->where('id', '!=', $pose->id)
|
||||
->where(function ($q) use ($pose, $tokens) {
|
||||
if ($pose->category_id) {
|
||||
$q->orWhere('category_id', $pose->category_id);
|
||||
}
|
||||
foreach ($tokens as $tk) {
|
||||
$q->orWhere('benefits', 'like', "%{$tk}%");
|
||||
}
|
||||
})
|
||||
->orderBy('english_name')
|
||||
->limit(9)
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'pose' => new YogaPoseResource($pose),
|
||||
'similar_poses' => YogaPoseResource::collection($similarPoses),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,25 +3,48 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
* Mark the user's email address as verified.
|
||||
*
|
||||
* Deliberately not session-gated: the link is opened from an email
|
||||
* client, which may belong to a device/browser with no web session at
|
||||
* all (e.g. an account registered through the Android app). The signed
|
||||
* URL + hash-of-email check below is what proves identity here, not
|
||||
* Auth::user().
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
public function __invoke(Request $request, int $id, string $hash): RedirectResponse|\Illuminate\Http\JsonResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
if (!hash_equals(sha1($user->getEmailForVerification()), $hash)) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$alreadyVerified = $user->hasVerifiedEmail();
|
||||
|
||||
if (!$alreadyVerified && $user->markEmailAsVerified()) {
|
||||
event(new Verified($user));
|
||||
}
|
||||
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json([
|
||||
'message' => $alreadyVerified ? 'Email already verified.' : 'Email verified.',
|
||||
'verified' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
if (Auth::check() && Auth::id() === $user->id) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
return redirect()->route('login')->with('status', 'Email verified. You can now log in.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ExerciseResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->display_name,
|
||||
'original_name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'gif_url' => $this->gif_url,
|
||||
'poster_url' => $this->poster_url,
|
||||
'instructions' => $this->instructions,
|
||||
'body_parts' => $this->whenLoaded('bodyParts', fn () => $this->bodyParts->map(fn ($m) => [
|
||||
'id' => $m->id, 'name' => $m->name, 'slug' => $m->slug,
|
||||
])),
|
||||
'target_muscles' => $this->whenLoaded('targetMuscles', fn () => $this->targetMuscles->map(fn ($m) => [
|
||||
'id' => $m->id, 'name' => $m->name, 'slug' => $m->slug,
|
||||
])),
|
||||
'secondary_muscles' => $this->whenLoaded('secondaryMuscles', fn () => $this->secondaryMuscles->map(fn ($m) => [
|
||||
'id' => $m->id, 'name' => $m->name, 'slug' => $m->slug,
|
||||
])),
|
||||
'equipments' => $this->whenLoaded('equipments', fn () => $this->equipments->map(fn ($m) => [
|
||||
'id' => $m->id, 'name' => $m->name, 'slug' => $m->slug,
|
||||
])),
|
||||
'is_favorited' => $request->user()
|
||||
? $request->user()->favoriteExercises()->where('exercise_id', $this->id)->exists()
|
||||
: false,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ProgramResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'user_id' => $this->user_id,
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'weeks' => $this->weeks,
|
||||
'workouts_count' => $this->whenCounted('workouts'),
|
||||
'workouts' => WorkoutResource::collection($this->whenLoaded('workouts')),
|
||||
'created_at' => $this->created_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class UserResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'email' => $this->email,
|
||||
'is_trainer' => (bool) $this->is_trainer,
|
||||
'is_admin' => (bool) $this->is_admin,
|
||||
'age' => $this->age,
|
||||
'weight_kg' => $this->weight_kg !== null ? (float) $this->weight_kg : null,
|
||||
'height_cm' => $this->height_cm !== null ? (float) $this->height_cm : null,
|
||||
'fitness_level' => $this->fitness_level,
|
||||
'goal' => $this->goal,
|
||||
'health_notes' => $this->health_notes,
|
||||
'available_equipment' => $this->available_equipment,
|
||||
'days_per_week' => $this->days_per_week,
|
||||
'minutes_per_session' => $this->minutes_per_session,
|
||||
'created_at' => $this->created_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class WeightLogResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'weight_kg' => (float) $this->weight_kg,
|
||||
'logged_at' => $this->logged_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class WorkoutExerciseResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'workout_id' => $this->workout_id,
|
||||
'position' => $this->position,
|
||||
'sets' => $this->sets,
|
||||
'reps' => $this->reps,
|
||||
'weight' => $this->weight !== null ? (float) $this->weight : null,
|
||||
'rest_seconds' => $this->rest_seconds,
|
||||
'notes' => $this->notes,
|
||||
'exercise' => new ExerciseResource($this->whenLoaded('exercise')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class WorkoutResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'user_id' => $this->user_id,
|
||||
'owner' => new UserResource($this->whenLoaded('user')),
|
||||
'title' => $this->title,
|
||||
'notes' => $this->notes,
|
||||
'estimated_minutes' => $this->estimated_minutes,
|
||||
'is_public' => (bool) $this->is_public,
|
||||
'program_id' => $this->program_id,
|
||||
'week_number' => $this->week_number,
|
||||
'day_number' => $this->day_number,
|
||||
'assigned_by_trainer_id' => $this->assigned_by_trainer_id,
|
||||
'assigned_by_trainer' => new UserResource($this->whenLoaded('assignedByTrainer')),
|
||||
'items' => WorkoutExerciseResource::collection($this->whenLoaded('items')),
|
||||
'is_favorited' => $request->user()
|
||||
? $request->user()->favoriteWorkouts()->where('workout_id', $this->id)->exists()
|
||||
: false,
|
||||
'created_at' => $this->created_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class WorkoutSessionItemResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'session_id' => $this->session_id,
|
||||
'workout_exercise_id' => $this->workout_exercise_id,
|
||||
'done' => (bool) $this->done,
|
||||
'actual_weight' => $this->actual_weight !== null ? (float) $this->actual_weight : null,
|
||||
'actual_reps' => $this->actual_reps,
|
||||
'exercise_ms' => $this->exercise_ms,
|
||||
'rest_ms' => $this->rest_ms,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class WorkoutSessionResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'user_id' => $this->user_id,
|
||||
'workout_id' => $this->workout_id,
|
||||
'workout' => new WorkoutResource($this->whenLoaded('workout')),
|
||||
'started_at' => $this->started_at,
|
||||
'completed_at' => $this->completed_at,
|
||||
'items' => WorkoutSessionItemResource::collection($this->whenLoaded('items')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class YogaPoseResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'slug' => $this->slug,
|
||||
'english_name' => $this->english_name,
|
||||
'sanskrit_name' => $this->sanskrit_name,
|
||||
'sanskrit_name_adapted' => $this->sanskrit_name_adapted,
|
||||
'translation_name' => $this->translation_name,
|
||||
'description' => $this->description,
|
||||
'benefits' => $this->benefits,
|
||||
'url_svg' => $this->url_svg,
|
||||
'url_png' => $this->url_png,
|
||||
'url_svg_alt' => $this->url_svg_alt,
|
||||
'category' => $this->whenLoaded('category', fn () => [
|
||||
'id' => $this->category->id,
|
||||
'name' => $this->category->name,
|
||||
]),
|
||||
'is_favorited' => $request->user()
|
||||
? $request->user()->favoriteYogaPoses()->where('yoga_pose_id', $this->id)->exists()
|
||||
: false,
|
||||
];
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -6,11 +6,12 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Http\Middleware\SetLocale;
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"dedoc/scramble": "^0.13.39",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.3",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
Generated
+316
-64
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "bdc6d92507d397adf1c269ed49ec424d",
|
||||
"content-hash": "12cb787cf7a13805bce4ca75436679f4",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -135,6 +135,87 @@
|
||||
],
|
||||
"time": "2024-02-09T16:56:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dedoc/scramble",
|
||||
"version": "v0.13.39",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dedoc/scramble.git",
|
||||
"reference": "7fcc4758d62f22d326637a578168bd5eb67a8625"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dedoc/scramble/zipball/7fcc4758d62f22d326637a578168bd5eb67a8625",
|
||||
"reference": "7fcc4758d62f22d326637a578168bd5eb67a8625",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
|
||||
"myclabs/deep-copy": "^1.12",
|
||||
"nikic/php-parser": "^5.0",
|
||||
"php": "^8.1",
|
||||
"phpstan/phpdoc-parser": "^1.0|^2.0",
|
||||
"spatie/laravel-package-tools": "^1.9.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"larastan/larastan": "^3.3",
|
||||
"laravel/pint": "^v1.1.0",
|
||||
"laravel/scout": "^10.0|^11.0",
|
||||
"nunomaduro/collision": "^7.0|^8.0",
|
||||
"orchestra/testbench": "^8.0|^9.0|^10.0|^11.0",
|
||||
"pestphp/pest": "^2.34|^3.7|^4.4",
|
||||
"pestphp/pest-plugin-laravel": "^2.3|^3.1|^4.1",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^10.5|^11.5.3|^12.5.12",
|
||||
"spatie/laravel-permission": "^6.10|^7.2",
|
||||
"spatie/pest-plugin-snapshots": "^2.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Dedoc\\Scramble\\ScrambleServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dedoc\\Scramble\\": "src",
|
||||
"Dedoc\\Scramble\\Database\\Factories\\": "database/factories"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Roman Lytvynenko",
|
||||
"email": "litvinenko95@gmail.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Automatic generation of API documentation for Laravel applications.",
|
||||
"homepage": "https://github.com/dedoc/scramble",
|
||||
"keywords": [
|
||||
"documentation",
|
||||
"laravel",
|
||||
"openapi"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/dedoc/scramble/issues",
|
||||
"source": "https://github.com/dedoc/scramble/tree/v0.13.39"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/romalytvynenko",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-06T06:42:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dflydev/dot-access-data",
|
||||
"version": "v3.0.3",
|
||||
@@ -1331,6 +1412,69 @@
|
||||
},
|
||||
"time": "2025-09-19T13:47:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/sanctum",
|
||||
"version": "v4.3.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/sanctum.git",
|
||||
"reference": "fee27a573d1a013af3721d86153a65e0b11927e6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fee27a573d1a013af3721d86153a65e0b11927e6",
|
||||
"reference": "fee27a573d1a013af3721d86153a65e0b11927e6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"illuminate/console": "^11.0|^12.0|^13.0",
|
||||
"illuminate/contracts": "^11.0|^12.0|^13.0",
|
||||
"illuminate/database": "^11.0|^12.0|^13.0",
|
||||
"illuminate/support": "^11.0|^12.0|^13.0",
|
||||
"php": "^8.2",
|
||||
"symfony/console": "^7.0|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.6",
|
||||
"orchestra/testbench": "^9.15|^10.8|^11.0",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Sanctum\\SanctumServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Sanctum\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
|
||||
"keywords": [
|
||||
"auth",
|
||||
"laravel",
|
||||
"sanctum"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/sanctum/issues",
|
||||
"source": "https://github.com/laravel/sanctum"
|
||||
},
|
||||
"time": "2026-06-23T18:26:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/serializable-closure",
|
||||
"version": "v2.0.5",
|
||||
@@ -2112,6 +2256,66 @@
|
||||
],
|
||||
"time": "2025-03-24T10:02:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "myclabs/deep-copy",
|
||||
"version": "1.13.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/collections": "<1.6.8",
|
||||
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/collections": "^1.6.8",
|
||||
"doctrine/common": "^2.13.3 || ^3.2.2",
|
||||
"phpspec/prophecy": "^1.10",
|
||||
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/DeepCopy/deep_copy.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"DeepCopy\\": "src/DeepCopy/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Create deep copies (clones) of your objects",
|
||||
"keywords": [
|
||||
"clone",
|
||||
"copy",
|
||||
"duplicate",
|
||||
"object",
|
||||
"object graph"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
||||
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-01T08:46:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
"version": "3.10.3",
|
||||
@@ -2588,6 +2792,53 @@
|
||||
],
|
||||
"time": "2025-08-21T11:53:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
"version": "2.3.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpdoc-parser.git",
|
||||
"reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3",
|
||||
"reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/annotations": "^2.0",
|
||||
"nikic/php-parser": "^5.3.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"symfony/process": "^5.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPStan\\PhpDocParser\\": [
|
||||
"src/"
|
||||
]
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHPDoc parser with support for nullable, intersection and generic types",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3"
|
||||
},
|
||||
"time": "2026-07-08T07:01:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/clock",
|
||||
"version": "1.0.0",
|
||||
@@ -3276,6 +3527,67 @@
|
||||
},
|
||||
"time": "2025-09-04T20:59:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-package-tools",
|
||||
"version": "1.93.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-package-tools.git",
|
||||
"reference": "d5552849801f2642aea710557463234b59ef65eb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d5552849801f2642aea710557463234b59ef65eb",
|
||||
"reference": "d5552849801f2642aea710557463234b59ef65eb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.5",
|
||||
"orchestra/testbench": "^8.0|^9.2|^10.0|^11.0",
|
||||
"pestphp/pest": "^2.1|^3.1|^4.0",
|
||||
"phpunit/php-code-coverage": "^10.0|^11.0|^12.0",
|
||||
"phpunit/phpunit": "^10.5|^11.5|^12.5",
|
||||
"spatie/pest-plugin-test-time": "^2.2|^3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\LaravelPackageTools\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van der Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Tools for creating Laravel packages",
|
||||
"homepage": "https://github.com/spatie/laravel-package-tools",
|
||||
"keywords": [
|
||||
"laravel-package-tools",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/laravel-package-tools/issues",
|
||||
"source": "https://github.com/spatie/laravel-package-tools/tree/1.93.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-19T14:06:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
"version": "v7.3.0",
|
||||
@@ -6563,66 +6875,6 @@
|
||||
},
|
||||
"time": "2024-05-16T03:13:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "myclabs/deep-copy",
|
||||
"version": "1.13.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/collections": "<1.6.8",
|
||||
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/collections": "^1.6.8",
|
||||
"doctrine/common": "^2.13.3 || ^3.2.2",
|
||||
"phpspec/prophecy": "^1.10",
|
||||
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/DeepCopy/deep_copy.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"DeepCopy\\": "src/DeepCopy/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Create deep copies (clones) of your objects",
|
||||
"keywords": [
|
||||
"clone",
|
||||
"copy",
|
||||
"duplicate",
|
||||
"object",
|
||||
"object graph"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
||||
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-01T08:46:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nunomaduro/collision",
|
||||
"version": "v8.8.2",
|
||||
@@ -8451,12 +8703,12 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"stability-flags": [],
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.2"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.2.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stateful Domains
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Requests from the following domains / hosts will receive stateful API
|
||||
| authentication cookies. Typically, these should include your local
|
||||
| and production domains which access your API via a frontend SPA.
|
||||
|
|
||||
*/
|
||||
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'%s%s',
|
||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
Sanctum::currentApplicationUrlWithPort(),
|
||||
// Sanctum::currentRequestHost(),
|
||||
))),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This array contains the authentication guards that will be checked when
|
||||
| Sanctum is trying to authenticate a request. If none of these guards
|
||||
| are able to authenticate the request, Sanctum will use the bearer
|
||||
| token that's present on an incoming request for authentication.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expiration Minutes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the number of minutes until an issued token will be
|
||||
| considered expired. This will override any values set in the token's
|
||||
| "expires_at" attribute, but first-party sessions are not affected.
|
||||
|
|
||||
*/
|
||||
|
||||
'expiration' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Token Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||
| security scanning initiatives maintained by open source platforms
|
||||
| that notify developers if they commit tokens into repositories.
|
||||
|
|
||||
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||
|
|
||||
*/
|
||||
|
||||
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When authenticating your first-party SPA with Sanctum you may need to
|
||||
| customize some of the middleware Sanctum uses while processing the
|
||||
| request. You may change the middleware listed below as required.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => [
|
||||
'authenticate_session' => AuthenticateSession::class,
|
||||
'encrypt_cookies' => EncryptCookies::class,
|
||||
'validate_csrf_token' => ValidateCsrfToken::class,
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
|
||||
|
||||
return [
|
||||
/*
|
||||
* Which routes to document. String or array form; use Scramble::routes() for custom selection.
|
||||
*
|
||||
* 'api_path' => [
|
||||
* 'include' => 'api',
|
||||
* 'exclude' => ['api/internal'],
|
||||
* ],
|
||||
*
|
||||
* Without *, patterns match path segments (api matches api and api/users, not apiary).
|
||||
* With *, Str::is is used (e.g. api/v*).
|
||||
*
|
||||
* One static include → default server is /{include} and paths are stripped (/users).
|
||||
* Multiple includes or wildcards → server defaults to / and paths stay full (/api/users).
|
||||
* Override with `servers`, or use Scramble::registerApi() for separate bases.
|
||||
*/
|
||||
'api_path' => 'api',
|
||||
|
||||
/*
|
||||
* Your API domain. By default, app domain is used. This is also a part of the default API routes
|
||||
* matcher, so when implementing your own, make sure you use this config if needed.
|
||||
*/
|
||||
'api_domain' => null,
|
||||
|
||||
/*
|
||||
* The path where your OpenAPI specification will be exported.
|
||||
*/
|
||||
'export_path' => 'api.json',
|
||||
|
||||
/*
|
||||
* Cache configuration for the generated OpenAPI document.
|
||||
*
|
||||
* Use `scramble:cache` to warm the cache and `scramble:clear` to invalidate it.
|
||||
*/
|
||||
'cache' => [
|
||||
'key' => 'scramble.openapi',
|
||||
'store' => 'file',
|
||||
],
|
||||
|
||||
'info' => [
|
||||
/*
|
||||
* API version.
|
||||
*/
|
||||
'version' => env('API_VERSION', 'v1'),
|
||||
|
||||
/*
|
||||
* Description rendered on the home page of the API documentation (`/docs/api`).
|
||||
*/
|
||||
'description' => 'REST API for the fitness app, consumed by the Android client. All endpoints are namespaced under /api/v1. Auth is Sanctum bearer tokens obtained from POST /api/v1/login or /api/v1/register.',
|
||||
],
|
||||
|
||||
'ui' => [
|
||||
'title' => 'Fitness API',
|
||||
],
|
||||
|
||||
'renderer' => 'elements',
|
||||
|
||||
'renderers' => [
|
||||
/*
|
||||
* Stoplight Elements config options: https://docs.stoplight.io/docs/elements/b074dc47b2826-elements-configuration-options
|
||||
*/
|
||||
'elements' => [
|
||||
'view' => 'scramble::docs',
|
||||
'theme' => 'light',
|
||||
'hideTryIt' => false,
|
||||
'hideSchemas' => false,
|
||||
'logo' => '',
|
||||
'tryItCredentialsPolicy' => 'include',
|
||||
'layout' => 'responsive',
|
||||
'router' => 'hash',
|
||||
],
|
||||
/*
|
||||
* Scalar API reference config options: https://scalar.com/products/api-references/configuration
|
||||
*/
|
||||
'scalar' => [
|
||||
'view' => 'scramble::scalar',
|
||||
'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference',
|
||||
'theme' => 'laravel',
|
||||
'proxyUrl' => 'https://proxy.scalar.com',
|
||||
'darkMode' => false,
|
||||
'showDeveloperTools' => 'never',
|
||||
'agent' => ['disabled' => true],
|
||||
'credentials' => 'include',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
* The list of servers of the API. By default, when `null`, server URL will be created from
|
||||
* `scramble.api_path` and `scramble.api_domain` config variables. When providing an array, you
|
||||
* will need to specify the local server URL manually (if needed).
|
||||
*
|
||||
* Example of non-default config (final URLs are generated using Laravel `url` helper):
|
||||
*
|
||||
* ```php
|
||||
* 'servers' => [
|
||||
* 'Live' => 'api',
|
||||
* 'Prod' => 'https://scramble.dedoc.co/api',
|
||||
* ],
|
||||
* ```
|
||||
*/
|
||||
'servers' => null,
|
||||
|
||||
/**
|
||||
* Determines how Scramble stores the descriptions of enum cases.
|
||||
* Available options:
|
||||
* - 'description' – Case descriptions are stored as the enum schema's description using table formatting.
|
||||
* - 'extension' – Case descriptions are stored in the `x-enumDescriptions` enum schema extension.
|
||||
*
|
||||
* @see https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-enum-descriptions
|
||||
* - false - Case descriptions are ignored.
|
||||
*/
|
||||
'enum_cases_description_strategy' => 'description',
|
||||
|
||||
/**
|
||||
* Determines how Scramble stores the names of enum cases.
|
||||
* Available options:
|
||||
* - 'names' – Case names are stored in the `x-enumNames` enum schema extension.
|
||||
* - 'varnames' - Case names are stored in the `x-enum-varnames` enum schema extension.
|
||||
* - false - Case names are not stored.
|
||||
*/
|
||||
'enum_cases_names_strategy' => false,
|
||||
|
||||
/**
|
||||
* When Scramble encounters deep objects in query parameters, it flattens the parameters so the generated
|
||||
* OpenAPI document correctly describes the API. Flattening deep query parameters is relevant until
|
||||
* OpenAPI 3.2 is released and query string structure can be described properly.
|
||||
*
|
||||
* For example, this nested validation rule describes the object with `bar` property:
|
||||
* `['foo.bar' => ['required', 'int']]`.
|
||||
*
|
||||
* When `flatten_deep_query_parameters` is `true`, Scramble will document the parameter like so:
|
||||
* `{"name":"foo[bar]", "schema":{"type":"int"}, "required":true}`.
|
||||
*
|
||||
* When `flatten_deep_query_parameters` is `false`, Scramble will document the parameter like so:
|
||||
* `{"name":"foo", "schema": {"type":"object", "properties":{"bar":{"type": "int"}}, "required": ["bar"]}, "required":true}`.
|
||||
*/
|
||||
'flatten_deep_query_parameters' => true,
|
||||
|
||||
'middleware' => [
|
||||
'web',
|
||||
RestrictedDocsAccess::class,
|
||||
],
|
||||
|
||||
'extensions' => [],
|
||||
|
||||
/*
|
||||
* Automatically document API security (OpenAPI `security` / `securitySchemes`) based on route
|
||||
* middleware.
|
||||
*
|
||||
* Disabled by default. Uncomment the line below to enable `MiddlewareAuthSecurityStrategy`.
|
||||
* When at least one documented route uses middleware matching the configured patterns (by default
|
||||
* `auth` and `auth:*`), bearer auth is applied globally. Routes without matching middleware are
|
||||
* marked as public (`security: []`).
|
||||
*
|
||||
* Set to `null` explicitly to disable. If you already configure security manually via
|
||||
* `afterOpenApiGenerated` / `extendOpenApi`, keep this disabled to avoid duplicate schemes.
|
||||
*
|
||||
* Customize with a class-string or [class, options]:
|
||||
*
|
||||
* 'security_strategy' => [
|
||||
* \Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy::class,
|
||||
* [
|
||||
* 'middleware' => ['auth', 'auth:*'],
|
||||
* 'scheme' => \Dedoc\Scramble\Support\Generator\SecurityScheme::http('bearer'),
|
||||
* ],
|
||||
* ],
|
||||
*/
|
||||
'security_strategy' => \Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy::class,
|
||||
];
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->text('name');
|
||||
$table->string('token', 64)->unique();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('v1')->group(base_path('routes/api_v1.php'));
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\AuthController;
|
||||
use App\Http\Controllers\Api\DashboardController;
|
||||
use App\Http\Controllers\Api\DeviceController;
|
||||
use App\Http\Controllers\Api\EmailVerificationController;
|
||||
use App\Http\Controllers\Api\ExerciseAliasController;
|
||||
use App\Http\Controllers\Api\ExerciseController;
|
||||
use App\Http\Controllers\Api\FavoriteController;
|
||||
use App\Http\Controllers\Api\PasswordResetController;
|
||||
use App\Http\Controllers\Api\ProfileController;
|
||||
use App\Http\Controllers\Api\ProgramController;
|
||||
use App\Http\Controllers\Api\TrainerController;
|
||||
use App\Http\Controllers\Api\WeightLogController;
|
||||
use App\Http\Controllers\Api\WorkoutController;
|
||||
use App\Http\Controllers\Api\WorkoutSessionController;
|
||||
use App\Http\Controllers\Api\YogaPoseController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::post('/register', [AuthController::class, 'register'])->name('api.v1.register');
|
||||
Route::post('/login', [AuthController::class, 'login'])->name('api.v1.login');
|
||||
|
||||
Route::post('/forgot-password', [PasswordResetController::class, 'forgotPassword'])
|
||||
->middleware('throttle:6,1')
|
||||
->name('api.v1.password.email');
|
||||
Route::post('/reset-password', [PasswordResetController::class, 'resetPassword'])
|
||||
->middleware('throttle:6,1')
|
||||
->name('api.v1.password.reset');
|
||||
|
||||
// Public catalog browsing (no auth required, matches web's public exercise/yoga pages)
|
||||
Route::get('/exercises', [ExerciseController::class, 'index'])->name('api.v1.exercises.index');
|
||||
Route::get('/exercises/{exercise:slug}', [ExerciseController::class, 'show'])->name('api.v1.exercises.show');
|
||||
|
||||
Route::get('/yoga', [YogaPoseController::class, 'index'])->name('api.v1.yoga.index');
|
||||
Route::get('/yoga/{pose:slug}', [YogaPoseController::class, 'show'])->name('api.v1.yoga.show');
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::post('/logout', [AuthController::class, 'logout'])->name('api.v1.logout');
|
||||
Route::get('/me', [AuthController::class, 'me'])->name('api.v1.me');
|
||||
|
||||
// Profile
|
||||
Route::get('/profile', [ProfileController::class, 'show'])->name('api.v1.profile.show');
|
||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('api.v1.profile.update');
|
||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('api.v1.profile.destroy');
|
||||
|
||||
// Dashboard
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('api.v1.dashboard');
|
||||
|
||||
// Email verification (resend). The verify link itself is a public,
|
||||
// signed route registered in routes/auth.php - it works without a
|
||||
// Sanctum token since it's opened from an email client.
|
||||
Route::post('/email/verification-notification', [EmailVerificationController::class, 'store'])
|
||||
->middleware('throttle:6,1')
|
||||
->name('api.v1.verification.send');
|
||||
|
||||
// Devices (Sanctum tokens)
|
||||
Route::get('/devices', [DeviceController::class, 'index'])->name('api.v1.devices.index');
|
||||
Route::delete('/devices/{tokenId}', [DeviceController::class, 'destroy'])->name('api.v1.devices.destroy');
|
||||
|
||||
// Favorites
|
||||
Route::post('/exercises/{exercise}/favorite', [FavoriteController::class, 'toggleExercise'])->name('api.v1.exercises.favorite');
|
||||
Route::post('/workouts/{workout}/favorite', [FavoriteController::class, 'toggleWorkout'])->name('api.v1.workouts.favorite');
|
||||
Route::post('/yoga/{pose}/favorite', [FavoriteController::class, 'toggleYogaPose'])->name('api.v1.yoga.favorite');
|
||||
|
||||
// Exercise alias (per-user rename)
|
||||
Route::post('/exercises/{exercise}/alias', [ExerciseAliasController::class, 'store'])->name('api.v1.exercises.alias');
|
||||
|
||||
// Body weight log
|
||||
Route::get('/weight-logs', [WeightLogController::class, 'index'])->name('api.v1.weight-logs.index');
|
||||
Route::post('/weight-logs', [WeightLogController::class, 'store'])->name('api.v1.weight-logs.store');
|
||||
Route::delete('/weight-logs/{weightLog}', [WeightLogController::class, 'destroy'])->name('api.v1.weight-logs.destroy');
|
||||
|
||||
// Workouts
|
||||
Route::prefix('/workouts')->group(function () {
|
||||
Route::get('/', [WorkoutController::class, 'index'])->name('api.v1.workouts.index');
|
||||
Route::get('/community', [WorkoutController::class, 'community'])->name('api.v1.workouts.community');
|
||||
Route::post('/', [WorkoutController::class, 'store'])->name('api.v1.workouts.store');
|
||||
Route::get('/{workout}', [WorkoutController::class, 'show'])->name('api.v1.workouts.show');
|
||||
Route::delete('/{workout}', [WorkoutController::class, 'destroy'])->name('api.v1.workouts.destroy');
|
||||
Route::post('/{workout}/toggle-public', [WorkoutController::class, 'togglePublic'])->name('api.v1.workouts.toggle-public');
|
||||
Route::post('/{workout}/copy', [WorkoutController::class, 'copy'])->name('api.v1.workouts.copy');
|
||||
|
||||
Route::get('/{workout}/exercises/search', [WorkoutController::class, 'searchExercises'])->name('api.v1.workouts.exercises.search');
|
||||
Route::post('/{workout}/exercises', [WorkoutController::class, 'addExercise'])->name('api.v1.workouts.exercises.add');
|
||||
Route::patch('/{workout}/exercises/{item}', [WorkoutController::class, 'updateExercise'])->name('api.v1.workouts.exercises.update');
|
||||
Route::delete('/{workout}/exercises/{item}', [WorkoutController::class, 'removeExercise'])->name('api.v1.workouts.exercises.remove');
|
||||
|
||||
Route::post('/{workout}/sessions', [WorkoutSessionController::class, 'store'])->name('api.v1.sessions.store');
|
||||
});
|
||||
|
||||
// Sessions
|
||||
Route::get('/sessions/{session}', [WorkoutSessionController::class, 'show'])->name('api.v1.sessions.show');
|
||||
Route::patch('/sessions/{session}/items/{item}', [WorkoutSessionController::class, 'toggleItem'])->name('api.v1.sessions.items.toggle');
|
||||
Route::post('/sessions/{session}/complete', [WorkoutSessionController::class, 'complete'])->name('api.v1.sessions.complete');
|
||||
|
||||
// Programs
|
||||
Route::prefix('/programs')->group(function () {
|
||||
Route::get('/', [ProgramController::class, 'index'])->name('api.v1.programs.index');
|
||||
Route::post('/', [ProgramController::class, 'store'])->name('api.v1.programs.store');
|
||||
Route::get('/{program}', [ProgramController::class, 'show'])->name('api.v1.programs.show');
|
||||
Route::delete('/{program}', [ProgramController::class, 'destroy'])->name('api.v1.programs.destroy');
|
||||
Route::post('/{program}/workouts', [ProgramController::class, 'addWorkout'])->name('api.v1.programs.workouts.add');
|
||||
});
|
||||
|
||||
// Trainer: manage clients and assign workouts to them
|
||||
Route::middleware('trainer')->prefix('/trainer')->group(function () {
|
||||
Route::get('/clients', [TrainerController::class, 'clients'])->name('api.v1.trainer.clients');
|
||||
Route::post('/clients', [TrainerController::class, 'addClient'])->name('api.v1.trainer.clients.add');
|
||||
Route::delete('/clients/{client}', [TrainerController::class, 'removeClient'])->name('api.v1.trainer.clients.remove');
|
||||
Route::post('/clients/{client}/workouts', [TrainerController::class, 'storeWorkout'])->name('api.v1.trainer.workouts.store');
|
||||
});
|
||||
});
|
||||
+8
-4
@@ -35,14 +35,18 @@ Route::middleware('guest')->group(function () {
|
||||
->name('password.store');
|
||||
});
|
||||
|
||||
// Not session-gated: this link is opened from an email client, which may not
|
||||
// carry the web session of whichever device/browser registered the account
|
||||
// (e.g. a user who signed up through the Android app). Identity is proven by
|
||||
// the signed URL + hash, same as the auth-gated version this replaced.
|
||||
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
|
||||
->middleware(['signed', 'throttle:6,1'])
|
||||
->name('verification.verify');
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::get('verify-email', EmailVerificationPromptController::class)
|
||||
->name('verification.notice');
|
||||
|
||||
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
|
||||
->middleware(['signed', 'throttle:6,1'])
|
||||
->name('verification.verify');
|
||||
|
||||
Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
|
||||
->middleware('throttle:6,1')
|
||||
->name('verification.send');
|
||||
|
||||
Reference in New Issue
Block a user